Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Biomanus_upload/.env.example +22 -0
- Biomanus_upload/.gitattributes +2 -0
- Biomanus_upload/.gitignore +236 -0
- Biomanus_upload/.pre-commit-config.yaml +41 -0
- Biomanus_upload/LICENSE +201 -0
- Biomanus_upload/README.md +322 -0
- Biomanus_upload/biomni/__init__.py +3 -0
- Biomanus_upload/biomni/agent/__init__.py +1 -0
- Biomanus_upload/biomni/agent/a1.py +0 -0
- Biomanus_upload/biomni/agent/env_collection.py +313 -0
- Biomanus_upload/biomni/agent/function_generator.py +119 -0
- Biomanus_upload/biomni/agent/qa_llm.py +50 -0
- Biomanus_upload/biomni/agent/react.py +465 -0
- Biomanus_upload/biomni/biorxiv_scripts/extract_biorxiv_tasks.py +404 -0
- Biomanus_upload/biomni/biorxiv_scripts/generate_function.py +72 -0
- Biomanus_upload/biomni/biorxiv_scripts/process_all_subjects.py +315 -0
- Biomanus_upload/biomni/config.py +119 -0
- Biomanus_upload/biomni/env_desc.py +221 -0
- Biomanus_upload/biomni/env_desc_cm.py +219 -0
- Biomanus_upload/biomni/eval/__init__.py +3 -0
- Biomanus_upload/biomni/eval/biomni_eval1.py +324 -0
- Biomanus_upload/biomni/graph/__init__.py +17 -0
- Biomanus_upload/biomni/graph/graph_rag.py +306 -0
- Biomanus_upload/biomni/graph/graph_router.py +674 -0
- Biomanus_upload/biomni/graph/planning_agent.py +1046 -0
- Biomanus_upload/biomni/graph/schema_extractor.py +1180 -0
- Biomanus_upload/biomni/graph/tool_graph.py +273 -0
- Biomanus_upload/biomni/graph/workflow_priors.py +147 -0
- Biomanus_upload/biomni/know_how/__init__.py +9 -0
- Biomanus_upload/biomni/know_how/loader.py +348 -0
- Biomanus_upload/biomni/know_how/resource/CRISPick_download_links.txt +238 -0
- Biomanus_upload/biomni/know_how/resource/addgene_grna_sequences.csv +322 -0
- Biomanus_upload/biomni/know_how/sgRNA_design_guide.md +419 -0
- Biomanus_upload/biomni/know_how/single_cell_annotation.md +223 -0
- Biomanus_upload/biomni/llm.py +276 -0
- Biomanus_upload/biomni/model/__init__.py +0 -0
- Biomanus_upload/biomni/model/query_rewriter.py +399 -0
- Biomanus_upload/biomni/model/retriever.py +502 -0
- Biomanus_upload/biomni/task/__init__.py +0 -0
- Biomanus_upload/biomni/task/base_task.py +18 -0
- Biomanus_upload/biomni/task/hle.py +146 -0
- Biomanus_upload/biomni/task/lab_bench.py +115 -0
- Biomanus_upload/biomni/tool/__init__.py +4 -0
- Biomanus_upload/biomni/tool/biochemistry.py +1026 -0
- Biomanus_upload/biomni/tool/bioengineering.py +1387 -0
- Biomanus_upload/biomni/tool/bioimaging.py +1394 -0
- Biomanus_upload/biomni/tool/biophysics.py +513 -0
- Biomanus_upload/biomni/tool/cancer_biology.py +1292 -0
- Biomanus_upload/biomni/tool/cell_biology.py +742 -0
- Biomanus_upload/biomni/tool/database.py +0 -0
Biomanus_upload/.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
|
Biomanus_upload/.gitattributes
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.bam filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.h5ad filter=lfs diff=lfs merge=lfs -text
|
Biomanus_upload/.gitignore
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/
|
| 235 |
+
/paper_reproduction_runs/
|
| 236 |
+
/bioagent-bench-runs/
|
Biomanus_upload/.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"]
|
Biomanus_upload/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.
|
Biomanus_upload/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Biomanus
|
| 2 |
+
|
| 3 |
+
Biomanus is a graph-guided extension of the Biomni A1 biomedical agent. It adds MCP server generation, MCP ToolGraph construction, GraphRAG-style tool routing, dynamic MCP registration, and benchmark runners for BioAgent-Bench and LAB-Bench.
|
| 4 |
+
|
| 5 |
+
This README explains how to install the environment, prepare MCP servers, build the MCP graph, and run the experiments.
|
| 6 |
+
|
| 7 |
+
## 1. Repository Layout
|
| 8 |
+
|
| 9 |
+
```text
|
| 10 |
+
biomni/ Core agent, tools, graph router, retriever, and utilities
|
| 11 |
+
biomni_env/ Conda environment files and setup scripts
|
| 12 |
+
demo_web/backend/tool2mcp.py Utility for converting bioinformatics tools into MCP servers
|
| 13 |
+
build_generated_mcp_graph.py Build MCP server catalogs and graph artifacts
|
| 14 |
+
run_bioagent_bench.py BioAgent-Bench runner
|
| 15 |
+
evaluate_bioagent_bench.py BioAgent-Bench evaluator
|
| 16 |
+
experiments/lab_bench/scripts/ LAB-Bench runners and graph utilities
|
| 17 |
+
graph_outputs/ Example prebuilt graph artifacts
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
Large runtime artifacts, downloaded data lakes, benchmark outputs, node modules, and Git history are intentionally not included in this upload package.
|
| 21 |
+
|
| 22 |
+
## 2. Install the Environment
|
| 23 |
+
|
| 24 |
+
Create and activate the Biomni environment:
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
cd /path/to/Biomanus
|
| 28 |
+
conda env create -f biomni_env/environment.yml
|
| 29 |
+
conda activate biomni_e1
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
For a smaller environment, use:
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
conda env create -f biomni_env/fixed_env.yml
|
| 36 |
+
conda activate biomni_e1
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
Install extra packages used by MCP generation and benchmark execution:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
pip install fastmcp mcp openai anthropic python-dotenv pymupdf pipreqs
|
| 43 |
+
pip install langchain langchain-openai langchain-anthropic langgraph
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
Because this upload package is source-first and does not require a wheel install, run commands from the repository root or export:
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
export PYTHONPATH="$PWD:$PYTHONPATH"
|
| 50 |
+
export BIOMNI_EXECUTION_ENV_PREFIX="$CONDA_PREFIX"
|
| 51 |
+
export BIOMNI_EXECUTION_PYTHON="$CONDA_PREFIX/bin/python"
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
If your experiments need command-line bioinformatics tools, install them into the same conda environment. The full upstream Biomni setup script is available at:
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
bash biomni_env/setup.sh
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
This full setup can take many hours and requires substantial disk space.
|
| 61 |
+
|
| 62 |
+
## 3. Configure the LLM Backend
|
| 63 |
+
|
| 64 |
+
Biomanus uses Biomni's `Custom` OpenAI-compatible backend in the main experiments. For DeepSeek:
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
export DEEPSEEK_API_KEY="your_key"
|
| 68 |
+
export DEEPSEEK_BASE_URL="https://api.deepseek.com/v1"
|
| 69 |
+
export DEEPSEEK_MODEL_NAME="deepseek-chat"
|
| 70 |
+
|
| 71 |
+
export BIOMNI_SOURCE="Custom"
|
| 72 |
+
export BIOMNI_LLM="$DEEPSEEK_MODEL_NAME"
|
| 73 |
+
export BIOMNI_CUSTOM_BASE_URL="$DEEPSEEK_BASE_URL"
|
| 74 |
+
export BIOMNI_CUSTOM_API_KEY="$DEEPSEEK_API_KEY"
|
| 75 |
+
export BIOMNI_LLM_PROVIDER="deepseek"
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
For another OpenAI-compatible endpoint, keep `BIOMNI_SOURCE=Custom` and replace the base URL, model name, and API key.
|
| 79 |
+
|
| 80 |
+
## 4. Prepare MCP Servers
|
| 81 |
+
|
| 82 |
+
Graph-based execution requires local MCP server directories. Each generated server should follow this structure:
|
| 83 |
+
|
| 84 |
+
```text
|
| 85 |
+
demo_web/backend/data/mcp_generated/
|
| 86 |
+
mcp_<server_name>/
|
| 87 |
+
app/
|
| 88 |
+
<server_name>_server.py
|
| 89 |
+
<server_name>_shim_server.py # optional but recommended
|
| 90 |
+
Dockerfile
|
| 91 |
+
docker-compose.yml
|
| 92 |
+
requirements.txt
|
| 93 |
+
environment.yaml
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
Create the MCP directories:
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
mkdir -p demo_web/backend/data/mcp_generated
|
| 100 |
+
mkdir -p demo_web/backend/data/merged_prefer_help_txt
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
### Option A: Copy Existing MCP Servers
|
| 104 |
+
|
| 105 |
+
If you already generated MCP servers elsewhere, copy the `mcp_*` directories into:
|
| 106 |
+
|
| 107 |
+
```text
|
| 108 |
+
demo_web/backend/data/mcp_generated/
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
Then patch generated servers so they expose stdio-compatible MCP entrypoints:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
python patch_raw_mcp_servers.py --mcp-root demo_web/backend/data/mcp_generated
|
| 115 |
+
python patch_generated_mcp_shims.py --mcp-root demo_web/backend/data/mcp_generated
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### Option B: Generate One MCP Server from a Tool Manual
|
| 119 |
+
|
| 120 |
+
Use `tool2mcp.py` to convert a bioinformatics command-line tool and its help/manual text into an MCP server.
|
| 121 |
+
|
| 122 |
+
Example using a local help text:
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
python demo_web/backend/tool2mcp.py \
|
| 126 |
+
--name kallisto \
|
| 127 |
+
--manual docs/kallisto_help.txt \
|
| 128 |
+
--output_location demo_web/backend/data/mcp_generated \
|
| 129 |
+
--llm_provider deepseek
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
Example using the installed tool's help command:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
python demo_web/backend/tool2mcp.py \
|
| 136 |
+
--name fastqc \
|
| 137 |
+
--manual=--help \
|
| 138 |
+
--run_help_command True \
|
| 139 |
+
--output_location demo_web/backend/data/mcp_generated \
|
| 140 |
+
--llm_provider deepseek
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
The converter calls an LLM, so the corresponding API key must be configured. Supported providers include `deepseek`, `openai`, `claude`, `azure`, and `gemini`.
|
| 144 |
+
|
| 145 |
+
## 5. Build the MCP Graph
|
| 146 |
+
|
| 147 |
+
Once MCP servers exist under `demo_web/backend/data/mcp_generated`, build a graph over all servers:
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
python build_generated_mcp_graph.py \
|
| 151 |
+
--mcp-root demo_web/backend/data/mcp_generated \
|
| 152 |
+
--help-root demo_web/backend/data/merged_prefer_help_txt \
|
| 153 |
+
--output-root graph_outputs \
|
| 154 |
+
--preset all \
|
| 155 |
+
--python-cmd "$BIOMNI_EXECUTION_PYTHON"
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
The output directory will look like:
|
| 159 |
+
|
| 160 |
+
```text
|
| 161 |
+
graph_outputs/mcp_generated_graph_all_<timestamp>/
|
| 162 |
+
server_catalog.json
|
| 163 |
+
graph_nodes.json
|
| 164 |
+
graph_edges.json
|
| 165 |
+
server_semantics.json
|
| 166 |
+
graph_summary.json
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
For BioAgent-Bench, you can build a smaller graph containing high-signal benchmark servers:
|
| 170 |
+
|
| 171 |
+
```bash
|
| 172 |
+
python build_generated_mcp_graph.py \
|
| 173 |
+
--mcp-root demo_web/backend/data/mcp_generated \
|
| 174 |
+
--help-root demo_web/backend/data/merged_prefer_help_txt \
|
| 175 |
+
--output-root graph_outputs \
|
| 176 |
+
--preset bioagent-bench \
|
| 177 |
+
--python-cmd "$BIOMNI_EXECUTION_PYTHON"
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
This upload package includes an example graph under `graph_outputs/`. Use it for inspection or as a template. For executable experiments on a new machine, rebuild the graph after placing MCP servers locally so `server_catalog.json` contains valid local commands.
|
| 181 |
+
|
| 182 |
+
## 6. Run BioAgent-Bench
|
| 183 |
+
|
| 184 |
+
First download or clone BioAgent-Bench and set the metadata/data paths:
|
| 185 |
+
|
| 186 |
+
```bash
|
| 187 |
+
export BIOAGENT_BENCH_DATASET_ROOT="/path/to/bioagent-bench/dataset"
|
| 188 |
+
export BIOAGENT_BENCH_METADATA="/path/to/bioagent-bench/src/task_metadata.json"
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
Run a single task:
|
| 192 |
+
|
| 193 |
+
```bash
|
| 194 |
+
python run_bioagent_bench.py \
|
| 195 |
+
--task transcript-quant \
|
| 196 |
+
--dataset-root "$BIOAGENT_BENCH_DATASET_ROOT" \
|
| 197 |
+
--metadata "$BIOAGENT_BENCH_METADATA" \
|
| 198 |
+
--mcp-graph graph_outputs/mcp_generated_graph_benchmark_<timestamp> \
|
| 199 |
+
--executable-mcp-only \
|
| 200 |
+
--output-root experiments/bioagent-bench-runs \
|
| 201 |
+
--execution-env-prefix "$CONDA_PREFIX"
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
Run all supported tasks:
|
| 205 |
+
|
| 206 |
+
```bash
|
| 207 |
+
python run_bioagent_bench.py \
|
| 208 |
+
--all \
|
| 209 |
+
--dataset-root "$BIOAGENT_BENCH_DATASET_ROOT" \
|
| 210 |
+
--metadata "$BIOAGENT_BENCH_METADATA" \
|
| 211 |
+
--mcp-graph graph_outputs/mcp_generated_graph_benchmark_<timestamp> \
|
| 212 |
+
--executable-mcp-only \
|
| 213 |
+
--output-root experiments/bioagent-bench-runs \
|
| 214 |
+
--execution-env-prefix "$CONDA_PREFIX"
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
Useful ablation switches:
|
| 218 |
+
|
| 219 |
+
```bash
|
| 220 |
+
--no-graph-retriever # disable graph-guided routing
|
| 221 |
+
--no-mcp # disable MCP registration/execution
|
| 222 |
+
--no-use-tool-retriever # disable internal Biomni tool retrieval
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
Evaluate BioAgent-Bench outputs:
|
| 226 |
+
|
| 227 |
+
```bash
|
| 228 |
+
python evaluate_bioagent_bench.py \
|
| 229 |
+
--all \
|
| 230 |
+
--runs-root experiments/bioagent-bench-runs \
|
| 231 |
+
--dataset-root "$BIOAGENT_BENCH_DATASET_ROOT" \
|
| 232 |
+
--judge-mode rule \
|
| 233 |
+
--output experiments/bioagent-bench-runs/latest_evaluation.json
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
For LLM-based judging, set the judge provider/key and use `--judge-mode llm` or `--judge-mode both`.
|
| 237 |
+
|
| 238 |
+
## 7. Run LAB-Bench
|
| 239 |
+
|
| 240 |
+
First install or clone LAB-Bench and set:
|
| 241 |
+
|
| 242 |
+
```bash
|
| 243 |
+
export LAB_BENCH_ROOT="/path/to/LAB-Bench"
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
Run a small debug pass:
|
| 247 |
+
|
| 248 |
+
```bash
|
| 249 |
+
python experiments/lab_bench/scripts/run_labbench_with_hypobioos.py \
|
| 250 |
+
--evals DbQA SeqQA \
|
| 251 |
+
--splits test \
|
| 252 |
+
--debug \
|
| 253 |
+
--graph-dir graph_outputs/mcp_generated_graph_all_<timestamp> \
|
| 254 |
+
--output-root experiments/lab_bench/results/debug \
|
| 255 |
+
--agent-root experiments/lab_bench/agent_runtime/debug \
|
| 256 |
+
--compact-results-path experiments/lab_bench/results/debug/results.jsonl \
|
| 257 |
+
--reasoning-log-path experiments/lab_bench/results/debug/reasoning.log \
|
| 258 |
+
--compact-output-only
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
Run the main DbQA/SeqQA protocol:
|
| 262 |
+
|
| 263 |
+
```bash
|
| 264 |
+
python experiments/lab_bench/scripts/run_labbench_with_hypobioos.py \
|
| 265 |
+
--evals DbQA SeqQA \
|
| 266 |
+
--splits test \
|
| 267 |
+
--dev-size 45 \
|
| 268 |
+
--test-size 315 \
|
| 269 |
+
--seed 20260514 \
|
| 270 |
+
--graph-dir graph_outputs/mcp_generated_graph_all_<timestamp> \
|
| 271 |
+
--mcp-server-top-k 8 \
|
| 272 |
+
--mcp-tool-top-k 12 \
|
| 273 |
+
--mcp-server-candidate-pool 24 \
|
| 274 |
+
--output-root experiments/lab_bench/results/main \
|
| 275 |
+
--agent-root experiments/lab_bench/agent_runtime/main \
|
| 276 |
+
--compact-results-path experiments/lab_bench/results/main/results.jsonl \
|
| 277 |
+
--reasoning-log-path experiments/lab_bench/results/main/reasoning.log \
|
| 278 |
+
--compact-output-only
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
For parallel sharded runs, call the same runner repeatedly with `--shard-index` and `--shard-count`, writing all shards to the same compact JSONL with `--skip-existing-results`.
|
| 282 |
+
|
| 283 |
+
## 8. Recommended Reproduction Order
|
| 284 |
+
|
| 285 |
+
1. Create and activate the conda environment.
|
| 286 |
+
2. Configure the LLM backend.
|
| 287 |
+
3. Generate or copy MCP servers into `demo_web/backend/data/mcp_generated`.
|
| 288 |
+
4. Patch MCP shims.
|
| 289 |
+
5. Build the MCP graph.
|
| 290 |
+
6. Run a single BioAgent-Bench task as a smoke test.
|
| 291 |
+
7. Run LAB-Bench in `--debug` mode.
|
| 292 |
+
8. Launch full benchmark runs and evaluate.
|
| 293 |
+
|
| 294 |
+
## 9. Common Issues
|
| 295 |
+
|
| 296 |
+
`MCP root does not exist`
|
| 297 |
+
|
| 298 |
+
Create `demo_web/backend/data/mcp_generated` and copy or generate `mcp_*` server directories.
|
| 299 |
+
|
| 300 |
+
`Help root does not exist`
|
| 301 |
+
|
| 302 |
+
Create an empty help directory:
|
| 303 |
+
|
| 304 |
+
```bash
|
| 305 |
+
mkdir -p demo_web/backend/data/merged_prefer_help_txt
|
| 306 |
+
```
|
| 307 |
+
|
| 308 |
+
`server_catalog.json points to old paths`
|
| 309 |
+
|
| 310 |
+
Rebuild the graph on the current machine after placing MCP servers locally. The graph stores executable command paths.
|
| 311 |
+
|
| 312 |
+
`Missing API key`
|
| 313 |
+
|
| 314 |
+
Export `DEEPSEEK_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or the matching provider key before running agents or MCP generation.
|
| 315 |
+
|
| 316 |
+
`LAB-Bench import errors`
|
| 317 |
+
|
| 318 |
+
Set `LAB_BENCH_ROOT` to the local LAB-Bench checkout and install LAB-Bench dependencies in the active environment.
|
| 319 |
+
|
| 320 |
+
`BioAgent-Bench cannot find metadata`
|
| 321 |
+
|
| 322 |
+
Set `BIOAGENT_BENCH_METADATA` and `BIOAGENT_BENCH_DATASET_ROOT`, or pass `--metadata` and `--dataset-root` explicitly.
|
Biomanus_upload/biomni/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .version import __version__
|
| 2 |
+
|
| 3 |
+
__all__ = ["__version__"]
|
Biomanus_upload/biomni/agent/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from biomni.agent.a1 import A1 # noqa: F401
|
Biomanus_upload/biomni/agent/a1.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Biomanus_upload/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
|
Biomanus_upload/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
|
Biomanus_upload/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
|
Biomanus_upload/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
|
Biomanus_upload/biomni/biorxiv_scripts/extract_biorxiv_tasks.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Script to extract common tasks, databases, and software from bioRxiv papers
|
| 3 |
+
using the PaperTaskExtractor and existing metadata CSV.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import io
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import random
|
| 11 |
+
import sys
|
| 12 |
+
import time
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
import pandas as pd
|
| 16 |
+
import PyPDF2
|
| 17 |
+
import requests
|
| 18 |
+
from tqdm import tqdm
|
| 19 |
+
|
| 20 |
+
# Add the parent directory to the path so we can import the bioagentos package
|
| 21 |
+
sys.path.append("../../")
|
| 22 |
+
from biomni.agent.env_collection import PaperTaskExtractor
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def parse_arguments():
|
| 26 |
+
"""Parse command line arguments."""
|
| 27 |
+
parser = argparse.ArgumentParser(description="Extract tasks from bioRxiv papers in a specific subject area.")
|
| 28 |
+
parser.add_argument(
|
| 29 |
+
"--subject",
|
| 30 |
+
type=str,
|
| 31 |
+
required=True,
|
| 32 |
+
help='Subject area to search for papers (e.g., "neuroscience", "bioinformatics")',
|
| 33 |
+
)
|
| 34 |
+
parser.add_argument(
|
| 35 |
+
"--limit",
|
| 36 |
+
type=int,
|
| 37 |
+
default=10,
|
| 38 |
+
help="Maximum number of papers to process (default: 10)",
|
| 39 |
+
)
|
| 40 |
+
parser.add_argument(
|
| 41 |
+
"--metadata-path",
|
| 42 |
+
type=str,
|
| 43 |
+
default="<REMOTE_USER_HOME>/BioAgentOS/data/biorxiv_metadata.csv",
|
| 44 |
+
help="Path to bioRxiv metadata CSV file (default: data/biorxiv_metadata.csv)",
|
| 45 |
+
)
|
| 46 |
+
parser.add_argument(
|
| 47 |
+
"--output-dir",
|
| 48 |
+
type=str,
|
| 49 |
+
default=None,
|
| 50 |
+
help="Directory to save results (default: ./biorxiv_results_{subject}_{limit})",
|
| 51 |
+
)
|
| 52 |
+
parser.add_argument(
|
| 53 |
+
"--model",
|
| 54 |
+
type=str,
|
| 55 |
+
default="claude-3-haiku-20240307",
|
| 56 |
+
help="LLM model to use for extraction (default: claude-3-haiku-20240307)",
|
| 57 |
+
)
|
| 58 |
+
parser.add_argument(
|
| 59 |
+
"--chunk-size",
|
| 60 |
+
type=int,
|
| 61 |
+
default=4000,
|
| 62 |
+
help="Chunk size for text processing (default: 4000)",
|
| 63 |
+
)
|
| 64 |
+
parser.add_argument(
|
| 65 |
+
"--chunk-overlap",
|
| 66 |
+
type=int,
|
| 67 |
+
default=400,
|
| 68 |
+
help="Chunk overlap for text processing (default: 400)",
|
| 69 |
+
)
|
| 70 |
+
parser.add_argument(
|
| 71 |
+
"--max-paper-length",
|
| 72 |
+
type=int,
|
| 73 |
+
default=200000,
|
| 74 |
+
help="Maximum paper length in characters (default: 200000)",
|
| 75 |
+
)
|
| 76 |
+
parser.add_argument("--save-pdfs", action="store_true", help="Save downloaded PDFs (default: False)")
|
| 77 |
+
parser.add_argument(
|
| 78 |
+
"--random-sample",
|
| 79 |
+
action="store_true",
|
| 80 |
+
help="Randomly sample papers instead of taking the first N (default: False)",
|
| 81 |
+
)
|
| 82 |
+
return parser.parse_args()
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def load_papers_from_csv(metadata_path: str, subject: str, limit: int, random_sample: bool) -> pd.DataFrame:
|
| 86 |
+
"""Load papers from bioRxiv metadata CSV file.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
metadata_path: Path to the metadata CSV file
|
| 90 |
+
subject: Subject area to filter by
|
| 91 |
+
limit: Maximum number of papers to return
|
| 92 |
+
random_sample: Whether to randomly sample papers
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
DataFrame of filtered papers
|
| 96 |
+
|
| 97 |
+
"""
|
| 98 |
+
try:
|
| 99 |
+
# Load the metadata CSV
|
| 100 |
+
df_biorxiv = pd.read_csv(metadata_path)
|
| 101 |
+
|
| 102 |
+
# Filter published papers in the specified subject
|
| 103 |
+
if subject.lower() == "all":
|
| 104 |
+
filtered_df = df_biorxiv[df_biorxiv.published != "NA"]
|
| 105 |
+
else:
|
| 106 |
+
filtered_df = df_biorxiv[
|
| 107 |
+
(df_biorxiv.published != "NA") & (df_biorxiv.category.str.lower() == subject.lower())
|
| 108 |
+
]
|
| 109 |
+
|
| 110 |
+
# Sample papers
|
| 111 |
+
if len(filtered_df) > limit:
|
| 112 |
+
filtered_df = filtered_df.sample(limit, random_state=42) if random_sample else filtered_df.head(limit)
|
| 113 |
+
|
| 114 |
+
print(f"Loaded {len(filtered_df)} papers in subject: {subject}")
|
| 115 |
+
return filtered_df
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
print(f"Error loading papers from CSV: {e}")
|
| 119 |
+
return pd.DataFrame()
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def download_pdf(paper_doi: str, save_path: str | None = None) -> str | None:
|
| 123 |
+
"""Download PDF for a given paper DOI.
|
| 124 |
+
|
| 125 |
+
Args:
|
| 126 |
+
paper_doi: DOI of the paper
|
| 127 |
+
save_path: Path to save the PDF (if None, won't save to disk)
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
PDF text content or None if download failed
|
| 131 |
+
|
| 132 |
+
"""
|
| 133 |
+
# Construct PDF URL from DOI
|
| 134 |
+
pdf_url = f"https://www.biorxiv.org/content/{paper_doi}v1.full.pdf"
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
# Add random delay to avoid rate limiting
|
| 138 |
+
time.sleep(random.uniform(1, 3))
|
| 139 |
+
|
| 140 |
+
response = requests.get(pdf_url, stream=True)
|
| 141 |
+
response.raise_for_status()
|
| 142 |
+
|
| 143 |
+
# Save PDF if requested
|
| 144 |
+
if save_path:
|
| 145 |
+
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
| 146 |
+
with open(save_path, "wb") as f:
|
| 147 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 148 |
+
f.write(chunk)
|
| 149 |
+
|
| 150 |
+
# Extract text from PDF
|
| 151 |
+
pdf_text = extract_text_from_pdf(io.BytesIO(response.content))
|
| 152 |
+
return pdf_text
|
| 153 |
+
|
| 154 |
+
except requests.exceptions.RequestException as e:
|
| 155 |
+
print(f"Error downloading PDF for {paper_doi}: {e}")
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def extract_text_from_pdf(pdf_file) -> str:
|
| 160 |
+
"""Extract text content from a PDF file.
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
pdf_file: File-like object containing PDF data
|
| 164 |
+
|
| 165 |
+
Returns:
|
| 166 |
+
Extracted text content
|
| 167 |
+
|
| 168 |
+
"""
|
| 169 |
+
try:
|
| 170 |
+
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
| 171 |
+
text = ""
|
| 172 |
+
for page_num in range(len(pdf_reader.pages)):
|
| 173 |
+
page = pdf_reader.pages[page_num]
|
| 174 |
+
text += page.extract_text() + "\n\n"
|
| 175 |
+
return text
|
| 176 |
+
except Exception as e:
|
| 177 |
+
print(f"Error extracting text from PDF: {e}")
|
| 178 |
+
return ""
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def truncate_text(text: str, max_length: int) -> str:
|
| 182 |
+
"""Truncate text to a maximum length while preserving complete sentences.
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
text: The text to truncate
|
| 186 |
+
max_length: Maximum length in characters
|
| 187 |
+
|
| 188 |
+
Returns:
|
| 189 |
+
Truncated text
|
| 190 |
+
|
| 191 |
+
"""
|
| 192 |
+
if len(text) <= max_length:
|
| 193 |
+
return text
|
| 194 |
+
|
| 195 |
+
# Find the last sentence boundary before max_length
|
| 196 |
+
truncated = text[:max_length]
|
| 197 |
+
last_period = truncated.rfind(".")
|
| 198 |
+
last_question = truncated.rfind("?")
|
| 199 |
+
last_exclamation = truncated.rfind("!")
|
| 200 |
+
|
| 201 |
+
# Find the last sentence boundary
|
| 202 |
+
last_boundary = max(last_period, last_question, last_exclamation)
|
| 203 |
+
|
| 204 |
+
if last_boundary > 0:
|
| 205 |
+
return text[: last_boundary + 1]
|
| 206 |
+
else:
|
| 207 |
+
# If no sentence boundary found, just truncate at max_length
|
| 208 |
+
return truncated
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def process_papers(papers_df: pd.DataFrame, args) -> list[dict[str, Any]]:
|
| 212 |
+
"""Process papers using PaperTaskExtractor.
|
| 213 |
+
|
| 214 |
+
Args:
|
| 215 |
+
papers_df: DataFrame of paper metadata
|
| 216 |
+
args: Command line arguments
|
| 217 |
+
|
| 218 |
+
Returns:
|
| 219 |
+
List of papers with extracted tasks, databases, and software
|
| 220 |
+
|
| 221 |
+
"""
|
| 222 |
+
# Initialize the task extractor
|
| 223 |
+
extractor = PaperTaskExtractor(llm=args.model, chunk_size=args.chunk_size, chunk_overlap=args.chunk_overlap)
|
| 224 |
+
|
| 225 |
+
results = []
|
| 226 |
+
|
| 227 |
+
for _, paper in tqdm(papers_df.iterrows(), total=len(papers_df), desc="Processing papers"):
|
| 228 |
+
paper_doi = paper.get("doi")
|
| 229 |
+
if not paper_doi:
|
| 230 |
+
continue
|
| 231 |
+
|
| 232 |
+
print(f"\nProcessing paper: {paper.get('title')} (DOI: {paper_doi})")
|
| 233 |
+
|
| 234 |
+
# Create paths
|
| 235 |
+
pdf_path = None
|
| 236 |
+
if args.save_pdfs:
|
| 237 |
+
pdf_path = os.path.join(args.output_dir, "pdfs", f"{paper_doi.replace('/', '_')}.pdf")
|
| 238 |
+
|
| 239 |
+
result_path = os.path.join(args.output_dir, "results", f"{paper_doi.replace('/', '_')}.json")
|
| 240 |
+
|
| 241 |
+
# Skip if already processed
|
| 242 |
+
if os.path.exists(result_path):
|
| 243 |
+
print(f"Paper already processed, loading from {result_path}")
|
| 244 |
+
with open(result_path) as f:
|
| 245 |
+
paper_result = json.load(f)
|
| 246 |
+
results.append(paper_result)
|
| 247 |
+
continue
|
| 248 |
+
|
| 249 |
+
# Download and process PDF
|
| 250 |
+
pdf_text = download_pdf(paper_doi, pdf_path)
|
| 251 |
+
if not pdf_text:
|
| 252 |
+
print(f"Failed to download or extract text from {paper_doi}")
|
| 253 |
+
continue
|
| 254 |
+
|
| 255 |
+
# Truncate text if it exceeds max length
|
| 256 |
+
if len(pdf_text) > args.max_paper_length:
|
| 257 |
+
original_length = len(pdf_text)
|
| 258 |
+
pdf_text = truncate_text(pdf_text, args.max_paper_length)
|
| 259 |
+
print(f"Truncated paper from {original_length} to {len(pdf_text)} characters")
|
| 260 |
+
|
| 261 |
+
# Extract tasks, databases, and software
|
| 262 |
+
try:
|
| 263 |
+
_, extraction_results = extractor.go(pdf_text)
|
| 264 |
+
|
| 265 |
+
# Add paper metadata to results
|
| 266 |
+
paper_result = {
|
| 267 |
+
"metadata": {
|
| 268 |
+
"doi": paper.get("doi"),
|
| 269 |
+
"title": paper.get("title"),
|
| 270 |
+
"authors": paper.get("authors", ""),
|
| 271 |
+
"abstract": paper.get("abstract", ""),
|
| 272 |
+
"date": paper.get("date", ""),
|
| 273 |
+
"category": paper.get("category", ""),
|
| 274 |
+
"text_length": len(pdf_text),
|
| 275 |
+
},
|
| 276 |
+
"extraction": extraction_results,
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
# Save results
|
| 280 |
+
os.makedirs(os.path.dirname(result_path), exist_ok=True)
|
| 281 |
+
with open(result_path, "w") as f:
|
| 282 |
+
json.dump(paper_result, f, indent=2)
|
| 283 |
+
|
| 284 |
+
results.append(paper_result)
|
| 285 |
+
|
| 286 |
+
except Exception as e:
|
| 287 |
+
print(f"Error processing paper {paper_doi}: {e}")
|
| 288 |
+
|
| 289 |
+
return results
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def generate_summary(results: list[dict[str, Any]], output_dir: str):
|
| 293 |
+
"""Generate summary of extracted tasks, databases, and software.
|
| 294 |
+
|
| 295 |
+
Args:
|
| 296 |
+
results: List of papers with extraction results
|
| 297 |
+
output_dir: Directory to save summary
|
| 298 |
+
|
| 299 |
+
"""
|
| 300 |
+
# Collect all tasks, databases, and software
|
| 301 |
+
all_tasks = []
|
| 302 |
+
all_databases = []
|
| 303 |
+
all_software = []
|
| 304 |
+
|
| 305 |
+
for paper in results:
|
| 306 |
+
extraction = paper.get("extraction", {})
|
| 307 |
+
|
| 308 |
+
# Add paper metadata to each item
|
| 309 |
+
paper_meta = paper.get("metadata", {})
|
| 310 |
+
paper_id = f"{paper_meta.get('title')} ({paper_meta.get('doi')})"
|
| 311 |
+
|
| 312 |
+
# Process tasks
|
| 313 |
+
for task in extraction.get("tasks", []):
|
| 314 |
+
task["paper"] = paper_id
|
| 315 |
+
all_tasks.append(task)
|
| 316 |
+
|
| 317 |
+
# Process databases
|
| 318 |
+
for db in extraction.get("databases", []):
|
| 319 |
+
db["paper"] = paper_id
|
| 320 |
+
all_databases.append(db)
|
| 321 |
+
|
| 322 |
+
# Process software
|
| 323 |
+
for sw in extraction.get("software", []):
|
| 324 |
+
sw["paper"] = paper_id
|
| 325 |
+
all_software.append(sw)
|
| 326 |
+
|
| 327 |
+
# Create summary dataframes
|
| 328 |
+
if all_tasks:
|
| 329 |
+
tasks_df = pd.DataFrame(all_tasks)
|
| 330 |
+
tasks_df.to_csv(os.path.join(output_dir, "tasks_summary.csv"), index=False)
|
| 331 |
+
|
| 332 |
+
if all_databases:
|
| 333 |
+
db_df = pd.DataFrame(all_databases)
|
| 334 |
+
db_df.to_csv(os.path.join(output_dir, "databases_summary.csv"), index=False)
|
| 335 |
+
|
| 336 |
+
if all_software:
|
| 337 |
+
sw_df = pd.DataFrame(all_software)
|
| 338 |
+
sw_df.to_csv(os.path.join(output_dir, "software_summary.csv"), index=False)
|
| 339 |
+
|
| 340 |
+
# Generate frequency counts
|
| 341 |
+
task_counts = {}
|
| 342 |
+
for task in all_tasks:
|
| 343 |
+
task_name = task.get("task_name")
|
| 344 |
+
if task_name:
|
| 345 |
+
task_counts[task_name] = task_counts.get(task_name, 0) + 1
|
| 346 |
+
|
| 347 |
+
db_counts = {}
|
| 348 |
+
for db in all_databases:
|
| 349 |
+
db_name = db.get("name")
|
| 350 |
+
if db_name:
|
| 351 |
+
db_counts[db_name] = db_counts.get(db_name, 0) + 1
|
| 352 |
+
|
| 353 |
+
sw_counts = {}
|
| 354 |
+
for sw in all_software:
|
| 355 |
+
sw_name = sw.get("name")
|
| 356 |
+
if sw_name:
|
| 357 |
+
sw_counts[sw_name] = sw_counts.get(sw_name, 0) + 1
|
| 358 |
+
|
| 359 |
+
# Save frequency counts
|
| 360 |
+
with open(os.path.join(output_dir, "frequency_summary.json"), "w") as f:
|
| 361 |
+
json.dump(
|
| 362 |
+
{"tasks": task_counts, "databases": db_counts, "software": sw_counts},
|
| 363 |
+
f,
|
| 364 |
+
indent=2,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
print(f"Summary files saved to {output_dir}")
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def main():
|
| 371 |
+
"""Main function to run the script."""
|
| 372 |
+
args = parse_arguments()
|
| 373 |
+
|
| 374 |
+
# Set default output directory if not specified
|
| 375 |
+
if args.output_dir is None:
|
| 376 |
+
# Clean subject name for directory name (replace spaces with underscores, lowercase)
|
| 377 |
+
clean_subject = args.subject.lower().replace(" ", "_").replace("/", "_")
|
| 378 |
+
args.output_dir = f"./biorxiv_results_{clean_subject}_{args.limit}"
|
| 379 |
+
|
| 380 |
+
# Create output directories
|
| 381 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 382 |
+
os.makedirs(os.path.join(args.output_dir, "results"), exist_ok=True)
|
| 383 |
+
if args.save_pdfs:
|
| 384 |
+
os.makedirs(os.path.join(args.output_dir, "pdfs"), exist_ok=True)
|
| 385 |
+
|
| 386 |
+
# Load papers from CSV
|
| 387 |
+
print(f"Loading papers from {args.metadata_path} in subject area: {args.subject}")
|
| 388 |
+
papers_df = load_papers_from_csv(args.metadata_path, args.subject, args.limit, args.random_sample)
|
| 389 |
+
|
| 390 |
+
if len(papers_df) == 0:
|
| 391 |
+
print(f"No papers found for subject: {args.subject}")
|
| 392 |
+
return
|
| 393 |
+
|
| 394 |
+
# Process papers
|
| 395 |
+
results = process_papers(papers_df, args)
|
| 396 |
+
|
| 397 |
+
# Generate summary
|
| 398 |
+
generate_summary(results, args.output_dir)
|
| 399 |
+
|
| 400 |
+
print("Done!")
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
if __name__ == "__main__":
|
| 404 |
+
main()
|
Biomanus_upload/biomni/biorxiv_scripts/generate_function.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Command-line tool to generate Python functions from task descriptions using the function_generator agent."""
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
from biomni.agent.function_generator import FunctionGenerator
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def main():
|
| 13 |
+
"""Main function for the command-line tool."""
|
| 14 |
+
parser = argparse.ArgumentParser(description="Generate Python functions given task descriptions")
|
| 15 |
+
parser.add_argument(
|
| 16 |
+
"--task",
|
| 17 |
+
"-t",
|
| 18 |
+
type=str,
|
| 19 |
+
help="JSON file containing a list of task descriptions",
|
| 20 |
+
)
|
| 21 |
+
parser.add_argument(
|
| 22 |
+
"--output-dir",
|
| 23 |
+
"-o",
|
| 24 |
+
type=str,
|
| 25 |
+
default="generated_functions",
|
| 26 |
+
help="Directory to save the generated functions (default: generated_functions)",
|
| 27 |
+
)
|
| 28 |
+
parser.add_argument(
|
| 29 |
+
"--model",
|
| 30 |
+
"-m",
|
| 31 |
+
type=str,
|
| 32 |
+
default="claude-3-7-sonnet-latest",
|
| 33 |
+
help="LLM model to use (default: claude-3-7-sonnet-latest)",
|
| 34 |
+
)
|
| 35 |
+
parser.add_argument(
|
| 36 |
+
"--temperature",
|
| 37 |
+
type=float,
|
| 38 |
+
default=0.7,
|
| 39 |
+
help="Temperature setting for the LLM (default: 0.7)",
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
args = parser.parse_args()
|
| 43 |
+
|
| 44 |
+
with open(args.task) as f_tasks:
|
| 45 |
+
task_list = json.load(f_tasks)
|
| 46 |
+
task_descriptions = list(task_list["tasks"])
|
| 47 |
+
|
| 48 |
+
# Create the output directory if it doesn't exist
|
| 49 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 50 |
+
|
| 51 |
+
# Initialize the function generator agent
|
| 52 |
+
function_generator = FunctionGenerator(llm=args.model, temperature=args.temperature)
|
| 53 |
+
|
| 54 |
+
if not task_list:
|
| 55 |
+
print("No tasks found.")
|
| 56 |
+
else:
|
| 57 |
+
# tqdm shows a progress bar, file names as description
|
| 58 |
+
for _i, desc in enumerate(tqdm(task_descriptions, desc="Generating Python scripts given task descriptions"), 1):
|
| 59 |
+
generated_script_name, generated_codes = function_generator.go(desc)
|
| 60 |
+
|
| 61 |
+
# Save results
|
| 62 |
+
result_path = os.path.join(args.output_dir, generated_script_name)
|
| 63 |
+
|
| 64 |
+
os.makedirs(os.path.dirname(result_path), exist_ok=True)
|
| 65 |
+
with open(result_path, "w") as f:
|
| 66 |
+
f.write(generated_codes)
|
| 67 |
+
|
| 68 |
+
print("DONE")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
main()
|
Biomanus_upload/biomni/biorxiv_scripts/process_all_subjects.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Script to process all bioRxiv subjects and extract computational tasks from the top 100 papers in each."""
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import os
|
| 6 |
+
import subprocess
|
| 7 |
+
import sys
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
import pandas as pd
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
|
| 13 |
+
# Add the parent directory to the path so we can import the bioagentos package
|
| 14 |
+
sys.path.append("<REMOTE_USER_HOME>/BioAgentOS/")
|
| 15 |
+
|
| 16 |
+
# List of all bioRxiv subjects
|
| 17 |
+
BIORXIV_SUBJECTS = [
|
| 18 |
+
"evolutionary biology",
|
| 19 |
+
"ecology",
|
| 20 |
+
"neuroscience",
|
| 21 |
+
"developmental biology",
|
| 22 |
+
"plant biology",
|
| 23 |
+
"microbiology",
|
| 24 |
+
"cancer biology",
|
| 25 |
+
"immunology",
|
| 26 |
+
"cell biology",
|
| 27 |
+
"biochemistry",
|
| 28 |
+
"genetics",
|
| 29 |
+
"bioinformatics",
|
| 30 |
+
"animal behavior and cognition",
|
| 31 |
+
"biophysics",
|
| 32 |
+
"genomics",
|
| 33 |
+
"systems biology",
|
| 34 |
+
"bioengineering",
|
| 35 |
+
"molecular biology",
|
| 36 |
+
"physiology",
|
| 37 |
+
"zoology",
|
| 38 |
+
"scientific communication and education",
|
| 39 |
+
"pathology",
|
| 40 |
+
"synthetic biology",
|
| 41 |
+
"paleontology",
|
| 42 |
+
"pharmacology and toxicology",
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def parse_arguments():
|
| 47 |
+
"""Parse command line arguments."""
|
| 48 |
+
parser = argparse.ArgumentParser(description="Process all bioRxiv subjects and extract tasks from papers.")
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"--papers-per-subject",
|
| 51 |
+
type=int,
|
| 52 |
+
default=100,
|
| 53 |
+
help="Number of papers to process per subject (default: 100)",
|
| 54 |
+
)
|
| 55 |
+
parser.add_argument(
|
| 56 |
+
"--metadata-path",
|
| 57 |
+
type=str,
|
| 58 |
+
default="<REMOTE_USER_HOME>/BioAgentOS/data/biorxiv_metadata.csv",
|
| 59 |
+
help="Path to bioRxiv metadata CSV file (default: data/biorxiv_metadata.csv)",
|
| 60 |
+
)
|
| 61 |
+
parser.add_argument(
|
| 62 |
+
"--output-dir",
|
| 63 |
+
type=str,
|
| 64 |
+
default="./biorxiv_all_subjects",
|
| 65 |
+
help="Base directory to save results (default: ./biorxiv_all_subjects)",
|
| 66 |
+
)
|
| 67 |
+
parser.add_argument(
|
| 68 |
+
"--model",
|
| 69 |
+
type=str,
|
| 70 |
+
default="claude-3-haiku-20240307",
|
| 71 |
+
help="LLM model to use for extraction (default: claude-3-haiku-20240307)",
|
| 72 |
+
)
|
| 73 |
+
parser.add_argument(
|
| 74 |
+
"--max-paper-length",
|
| 75 |
+
type=int,
|
| 76 |
+
default=200000,
|
| 77 |
+
help="Maximum paper length in characters (default: 200000)",
|
| 78 |
+
)
|
| 79 |
+
parser.add_argument(
|
| 80 |
+
"--chunk-size",
|
| 81 |
+
type=int,
|
| 82 |
+
default=10000,
|
| 83 |
+
help="Chunk size for text processing (default: 10000)",
|
| 84 |
+
)
|
| 85 |
+
parser.add_argument(
|
| 86 |
+
"--chunk-overlap",
|
| 87 |
+
type=int,
|
| 88 |
+
default=400,
|
| 89 |
+
help="Chunk overlap for text processing (default: 400)",
|
| 90 |
+
)
|
| 91 |
+
parser.add_argument("--save-pdfs", action="store_true", help="Save downloaded PDFs (default: False)")
|
| 92 |
+
parser.add_argument(
|
| 93 |
+
"--random-sample",
|
| 94 |
+
action="store_true",
|
| 95 |
+
help="Randomly sample papers instead of taking the first N (default: False)",
|
| 96 |
+
)
|
| 97 |
+
parser.add_argument(
|
| 98 |
+
"--subjects",
|
| 99 |
+
type=str,
|
| 100 |
+
nargs="+",
|
| 101 |
+
help="Specific subjects to process (default: all subjects)",
|
| 102 |
+
)
|
| 103 |
+
parser.add_argument(
|
| 104 |
+
"--delay",
|
| 105 |
+
type=int,
|
| 106 |
+
default=60,
|
| 107 |
+
help="Delay in seconds between processing subjects (default: 60)",
|
| 108 |
+
)
|
| 109 |
+
parser.add_argument(
|
| 110 |
+
"--summary-only",
|
| 111 |
+
action="store_true",
|
| 112 |
+
help="Only generate the combined summary (default: False)",
|
| 113 |
+
)
|
| 114 |
+
return parser.parse_args()
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def check_subject_availability(metadata_path, subjects):
|
| 118 |
+
"""Check which subjects have papers available in the metadata.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
metadata_path: Path to the metadata CSV file
|
| 122 |
+
subjects: List of subjects to check
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
List of subjects with available papers
|
| 126 |
+
|
| 127 |
+
"""
|
| 128 |
+
try:
|
| 129 |
+
df_biorxiv = pd.read_csv(metadata_path)
|
| 130 |
+
available_subjects = []
|
| 131 |
+
|
| 132 |
+
for subject in subjects:
|
| 133 |
+
count = len(
|
| 134 |
+
df_biorxiv[(df_biorxiv.published != "NA") & (df_biorxiv.category.str.lower() == subject.lower())]
|
| 135 |
+
)
|
| 136 |
+
if count > 0:
|
| 137 |
+
available_subjects.append((subject, count))
|
| 138 |
+
print(f"Subject '{subject}' has {count} papers available")
|
| 139 |
+
else:
|
| 140 |
+
print(f"Subject '{subject}' has no papers available")
|
| 141 |
+
|
| 142 |
+
return available_subjects
|
| 143 |
+
except Exception as e:
|
| 144 |
+
print(f"Error checking subject availability: {e}")
|
| 145 |
+
return []
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def process_subject(subject, args):
|
| 149 |
+
"""Process a single subject by calling the extract_biorxiv_tasks.py script.
|
| 150 |
+
|
| 151 |
+
Args:
|
| 152 |
+
subject: Subject to process
|
| 153 |
+
args: Command line arguments
|
| 154 |
+
|
| 155 |
+
"""
|
| 156 |
+
# Clean subject name for command line
|
| 157 |
+
subject.replace(" ", "\\ ")
|
| 158 |
+
|
| 159 |
+
# Build command
|
| 160 |
+
cmd = [
|
| 161 |
+
"python",
|
| 162 |
+
"BioAgentOS/bioagentos/scripts/extract_biorxiv_tasks.py",
|
| 163 |
+
"--subject",
|
| 164 |
+
f'"{subject}"',
|
| 165 |
+
"--limit",
|
| 166 |
+
str(args.papers_per_subject),
|
| 167 |
+
"--metadata-path",
|
| 168 |
+
args.metadata_path,
|
| 169 |
+
"--model",
|
| 170 |
+
args.model,
|
| 171 |
+
"--max-paper-length",
|
| 172 |
+
str(args.max_paper_length),
|
| 173 |
+
"--chunk-size",
|
| 174 |
+
str(args.chunk_size),
|
| 175 |
+
"--chunk-overlap",
|
| 176 |
+
str(args.chunk_overlap),
|
| 177 |
+
]
|
| 178 |
+
|
| 179 |
+
# Add optional arguments
|
| 180 |
+
if args.save_pdfs:
|
| 181 |
+
cmd.append("--save-pdfs")
|
| 182 |
+
if args.random_sample:
|
| 183 |
+
cmd.append("--random-sample")
|
| 184 |
+
|
| 185 |
+
# Convert command list to string
|
| 186 |
+
cmd_str = " ".join(cmd)
|
| 187 |
+
|
| 188 |
+
print(f"\n{'=' * 80}\nProcessing subject: {subject}\n{'=' * 80}")
|
| 189 |
+
print(f"Running command: {cmd_str}")
|
| 190 |
+
|
| 191 |
+
# Execute command
|
| 192 |
+
try:
|
| 193 |
+
subprocess.run(cmd_str, shell=True, check=True)
|
| 194 |
+
print(f"Successfully processed subject: {subject}")
|
| 195 |
+
except subprocess.CalledProcessError as e:
|
| 196 |
+
print(f"Error processing subject '{subject}': {e}")
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def combine_summaries(base_dir, subjects, papers_per_subject):
|
| 200 |
+
"""Combine frequency summaries from all subjects into a single summary.
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
base_dir: Base directory containing subject results
|
| 204 |
+
subjects: List of subjects that were processed
|
| 205 |
+
papers_per_subject: Number of papers processed per subject
|
| 206 |
+
|
| 207 |
+
"""
|
| 208 |
+
all_tasks = {}
|
| 209 |
+
all_databases = {}
|
| 210 |
+
all_software = {}
|
| 211 |
+
|
| 212 |
+
for subject, _ in subjects:
|
| 213 |
+
# Clean subject name for directory name
|
| 214 |
+
clean_subject = subject.lower().replace(" ", "_").replace("/", "_")
|
| 215 |
+
subject_dir = f"biorxiv_results_{clean_subject}_{papers_per_subject}"
|
| 216 |
+
summary_path = os.path.join(subject_dir, "frequency_summary.json")
|
| 217 |
+
|
| 218 |
+
try:
|
| 219 |
+
if os.path.exists(summary_path):
|
| 220 |
+
with open(summary_path) as f:
|
| 221 |
+
summary = json.load(f)
|
| 222 |
+
|
| 223 |
+
# Merge tasks
|
| 224 |
+
for task, count in summary.get("tasks", {}).items():
|
| 225 |
+
if task in all_tasks:
|
| 226 |
+
all_tasks[task] += count
|
| 227 |
+
else:
|
| 228 |
+
all_tasks[task] = count
|
| 229 |
+
|
| 230 |
+
# Merge databases
|
| 231 |
+
for db, count in summary.get("databases", {}).items():
|
| 232 |
+
if db in all_databases:
|
| 233 |
+
all_databases[db] += count
|
| 234 |
+
else:
|
| 235 |
+
all_databases[db] = count
|
| 236 |
+
|
| 237 |
+
# Merge software
|
| 238 |
+
for sw, count in summary.get("software", {}).items():
|
| 239 |
+
if sw in all_software:
|
| 240 |
+
all_software[sw] += count
|
| 241 |
+
else:
|
| 242 |
+
all_software[sw] = count
|
| 243 |
+
else:
|
| 244 |
+
print(f"Warning: Summary file not found for subject '{subject}'")
|
| 245 |
+
except Exception as e:
|
| 246 |
+
print(f"Error processing summary for subject '{subject}': {e}")
|
| 247 |
+
|
| 248 |
+
# Sort by frequency (descending)
|
| 249 |
+
all_tasks = dict(sorted(all_tasks.items(), key=lambda item: item[1], reverse=True))
|
| 250 |
+
all_databases = dict(sorted(all_databases.items(), key=lambda item: item[1], reverse=True))
|
| 251 |
+
all_software = dict(sorted(all_software.items(), key=lambda item: item[1], reverse=True))
|
| 252 |
+
|
| 253 |
+
# Create combined summary
|
| 254 |
+
combined_summary = {
|
| 255 |
+
"tasks": all_tasks,
|
| 256 |
+
"databases": all_databases,
|
| 257 |
+
"software": all_software,
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
# Save combined summary
|
| 261 |
+
combined_path = os.path.join(base_dir, "combined_summary.json")
|
| 262 |
+
os.makedirs(base_dir, exist_ok=True)
|
| 263 |
+
|
| 264 |
+
with open(combined_path, "w") as f:
|
| 265 |
+
json.dump(combined_summary, f, indent=2)
|
| 266 |
+
|
| 267 |
+
print(f"Combined summary saved to {combined_path}")
|
| 268 |
+
|
| 269 |
+
# Create CSV files for each category
|
| 270 |
+
tasks_df = pd.DataFrame(list(all_tasks.items()), columns=["Task", "Frequency"])
|
| 271 |
+
tasks_df.to_csv(os.path.join(base_dir, "tasks_frequency.csv"), index=False)
|
| 272 |
+
|
| 273 |
+
db_df = pd.DataFrame(list(all_databases.items()), columns=["Database", "Frequency"])
|
| 274 |
+
db_df.to_csv(os.path.join(base_dir, "databases_frequency.csv"), index=False)
|
| 275 |
+
|
| 276 |
+
sw_df = pd.DataFrame(list(all_software.items()), columns=["Software", "Frequency"])
|
| 277 |
+
sw_df.to_csv(os.path.join(base_dir, "software_frequency.csv"), index=False)
|
| 278 |
+
|
| 279 |
+
print(f"CSV summaries saved to {base_dir}")
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def main():
|
| 283 |
+
"""Main function to run the script."""
|
| 284 |
+
args = parse_arguments()
|
| 285 |
+
|
| 286 |
+
# Determine which subjects to process
|
| 287 |
+
subjects_to_process = args.subjects if args.subjects else BIORXIV_SUBJECTS
|
| 288 |
+
|
| 289 |
+
# Check which subjects have papers available
|
| 290 |
+
available_subjects = check_subject_availability(args.metadata_path, subjects_to_process)
|
| 291 |
+
|
| 292 |
+
if not available_subjects:
|
| 293 |
+
print("No subjects with available papers found. Exiting.")
|
| 294 |
+
return
|
| 295 |
+
|
| 296 |
+
if not args.summary_only:
|
| 297 |
+
# Process each subject
|
| 298 |
+
for subject, _count in tqdm(available_subjects, desc="Processing subjects"):
|
| 299 |
+
process_subject(subject, args)
|
| 300 |
+
|
| 301 |
+
# Add delay between subjects to avoid rate limiting
|
| 302 |
+
if args.delay > 0:
|
| 303 |
+
print(f"Waiting {args.delay} seconds before processing the next subject...")
|
| 304 |
+
time.sleep(args.delay)
|
| 305 |
+
|
| 306 |
+
# Combine summaries from all subjects
|
| 307 |
+
combine_summaries(args.output_dir, available_subjects, args.papers_per_subject)
|
| 308 |
+
|
| 309 |
+
print("All subjects processed successfully!")
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
if __name__ == "__main__":
|
| 313 |
+
import json # Import here to avoid circular import with the main script
|
| 314 |
+
|
| 315 |
+
main()
|
Biomanus_upload/biomni/config.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-6"
|
| 37 |
+
temperature: float = 0.7
|
| 38 |
+
|
| 39 |
+
# Tool settings
|
| 40 |
+
use_tool_retriever: bool = True
|
| 41 |
+
rewrite_user_query: bool = True
|
| 42 |
+
dynamic_mcp_registration: bool = True
|
| 43 |
+
mcp_server_top_k: int = 8
|
| 44 |
+
mcp_tool_top_k: int = 12
|
| 45 |
+
mcp_server_candidate_pool: int = 24
|
| 46 |
+
|
| 47 |
+
# Data licensing settings
|
| 48 |
+
commercial_mode: bool = False # If True, excludes non-commercial datasets
|
| 49 |
+
|
| 50 |
+
# Custom model settings (for custom LLM serving)
|
| 51 |
+
base_url: str | None = None
|
| 52 |
+
api_key: str | None = None # Only for custom models, not provider API keys
|
| 53 |
+
|
| 54 |
+
# LLM source (auto-detected if None)
|
| 55 |
+
source: str | None = None
|
| 56 |
+
|
| 57 |
+
# Third-party integrations
|
| 58 |
+
protocols_io_access_token: str | None = None
|
| 59 |
+
|
| 60 |
+
def __post_init__(self):
|
| 61 |
+
"""Load any environment variable overrides if they exist."""
|
| 62 |
+
# Check for environment variable overrides (optional)
|
| 63 |
+
# Support both old and new names for backwards compatibility
|
| 64 |
+
if os.getenv("BIOMNI_PATH") or os.getenv("BIOMNI_DATA_PATH"):
|
| 65 |
+
self.path = os.getenv("BIOMNI_PATH") or os.getenv("BIOMNI_DATA_PATH")
|
| 66 |
+
if os.getenv("BIOMNI_TIMEOUT_SECONDS"):
|
| 67 |
+
self.timeout_seconds = int(os.getenv("BIOMNI_TIMEOUT_SECONDS"))
|
| 68 |
+
if os.getenv("BIOMNI_LLM") or os.getenv("BIOMNI_LLM_MODEL"):
|
| 69 |
+
self.llm = os.getenv("BIOMNI_LLM") or os.getenv("BIOMNI_LLM_MODEL")
|
| 70 |
+
if os.getenv("BIOMNI_USE_TOOL_RETRIEVER"):
|
| 71 |
+
self.use_tool_retriever = os.getenv("BIOMNI_USE_TOOL_RETRIEVER").lower() == "true"
|
| 72 |
+
if os.getenv("BIOMNI_REWRITE_USER_QUERY"):
|
| 73 |
+
self.rewrite_user_query = os.getenv("BIOMNI_REWRITE_USER_QUERY").lower() == "true"
|
| 74 |
+
if os.getenv("BIOMNI_DYNAMIC_MCP_REGISTRATION"):
|
| 75 |
+
self.dynamic_mcp_registration = os.getenv("BIOMNI_DYNAMIC_MCP_REGISTRATION").lower() == "true"
|
| 76 |
+
if os.getenv("BIOMNI_MCP_SERVER_TOP_K"):
|
| 77 |
+
self.mcp_server_top_k = int(os.getenv("BIOMNI_MCP_SERVER_TOP_K"))
|
| 78 |
+
if os.getenv("BIOMNI_MCP_TOOL_TOP_K"):
|
| 79 |
+
self.mcp_tool_top_k = int(os.getenv("BIOMNI_MCP_TOOL_TOP_K"))
|
| 80 |
+
if os.getenv("BIOMNI_MCP_SERVER_CANDIDATE_POOL"):
|
| 81 |
+
self.mcp_server_candidate_pool = int(os.getenv("BIOMNI_MCP_SERVER_CANDIDATE_POOL"))
|
| 82 |
+
if os.getenv("BIOMNI_COMMERCIAL_MODE"):
|
| 83 |
+
self.commercial_mode = os.getenv("BIOMNI_COMMERCIAL_MODE").lower() == "true"
|
| 84 |
+
if os.getenv("BIOMNI_TEMPERATURE"):
|
| 85 |
+
self.temperature = float(os.getenv("BIOMNI_TEMPERATURE"))
|
| 86 |
+
if os.getenv("BIOMNI_CUSTOM_BASE_URL"):
|
| 87 |
+
self.base_url = os.getenv("BIOMNI_CUSTOM_BASE_URL")
|
| 88 |
+
if os.getenv("BIOMNI_CUSTOM_API_KEY"):
|
| 89 |
+
self.api_key = os.getenv("BIOMNI_CUSTOM_API_KEY")
|
| 90 |
+
if os.getenv("BIOMNI_SOURCE"):
|
| 91 |
+
self.source = os.getenv("BIOMNI_SOURCE")
|
| 92 |
+
|
| 93 |
+
# Protocols.io access token (prefer specific env vars)
|
| 94 |
+
env_token = os.getenv("PROTOCOLS_IO_ACCESS_TOKEN") or os.getenv("BIOMNI_PROTOCOLS_IO_ACCESS_TOKEN")
|
| 95 |
+
if env_token:
|
| 96 |
+
self.protocols_io_access_token = env_token
|
| 97 |
+
|
| 98 |
+
def to_dict(self) -> dict:
|
| 99 |
+
"""Convert config to dictionary for easy access."""
|
| 100 |
+
return {
|
| 101 |
+
"path": self.path,
|
| 102 |
+
"timeout_seconds": self.timeout_seconds,
|
| 103 |
+
"llm": self.llm,
|
| 104 |
+
"temperature": self.temperature,
|
| 105 |
+
"use_tool_retriever": self.use_tool_retriever,
|
| 106 |
+
"rewrite_user_query": self.rewrite_user_query,
|
| 107 |
+
"dynamic_mcp_registration": self.dynamic_mcp_registration,
|
| 108 |
+
"mcp_server_top_k": self.mcp_server_top_k,
|
| 109 |
+
"mcp_tool_top_k": self.mcp_tool_top_k,
|
| 110 |
+
"mcp_server_candidate_pool": self.mcp_server_candidate_pool,
|
| 111 |
+
"commercial_mode": self.commercial_mode,
|
| 112 |
+
"base_url": self.base_url,
|
| 113 |
+
"api_key": self.api_key,
|
| 114 |
+
"source": self.source,
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# Global default config instance (optional, for convenience)
|
| 119 |
+
default_config = BiomniConfig()
|
Biomanus_upload/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 |
+
}
|
Biomanus_upload/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 |
+
}
|
Biomanus_upload/biomni/eval/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .biomni_eval1 import BiomniEval1
|
| 2 |
+
|
| 3 |
+
__all__ = ["BiomniEval1"]
|
Biomanus_upload/biomni/eval/biomni_eval1.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BiomniEval1: Evaluation loader for Biomni tasks
|
| 3 |
+
|
| 4 |
+
This class provides a unified interface to evaluate user answers against ground truth
|
| 5 |
+
for all tasks in the BiomniEval1 benchmark.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import pandas as pd
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class BiomniEval1:
|
| 15 |
+
"""
|
| 16 |
+
Evaluation loader for BiomniEval1 benchmark
|
| 17 |
+
|
| 18 |
+
Usage:
|
| 19 |
+
evaluator = BiomniEval1('biomni_eval1_dataset.parquet')
|
| 20 |
+
score = evaluator.evaluate('gwas_causal_gene_opentargets', 0, 'BRCA1')
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(self):
|
| 24 |
+
"""
|
| 25 |
+
Initialize the BiomniEval1 evaluator
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
dataset_path: Path to the merged dataset parquet file
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
self.df = pd.read_parquet("hf://datasets/biomni/Eval1/biomni_eval1_dataset.parquet")
|
| 32 |
+
|
| 33 |
+
# Create index mapping for fast lookup using task_instance_id
|
| 34 |
+
self.instance_map = {}
|
| 35 |
+
for idx, row in self.df.iterrows():
|
| 36 |
+
key = (row["task_name"], row["task_instance_id"])
|
| 37 |
+
self.instance_map[key] = idx
|
| 38 |
+
|
| 39 |
+
print(f"Loaded BiomniEval1 dataset: {len(self.df)} instances across {self.df['task_name'].nunique()} tasks")
|
| 40 |
+
|
| 41 |
+
def evaluate(self, task_name: str, task_instance_id: int, user_answer: str) -> float:
|
| 42 |
+
"""
|
| 43 |
+
Evaluate a user's answer for a given task and instance
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
task_name: Name of the task (e.g., 'gwas_causal_gene_opentargets')
|
| 47 |
+
task_instance_id: Task-specific instance ID (not the global instance_id)
|
| 48 |
+
user_answer: User's answer (format depends on task)
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
float: Reward score (0.0 to 1.0)
|
| 52 |
+
"""
|
| 53 |
+
# Look up the instance in the dataset using task_instance_id
|
| 54 |
+
key = (task_name, task_instance_id)
|
| 55 |
+
if key not in self.instance_map:
|
| 56 |
+
raise ValueError(f"Instance not found: task={task_name}, task_instance_id={task_instance_id}")
|
| 57 |
+
|
| 58 |
+
df_idx = self.instance_map[key]
|
| 59 |
+
row = self.df.iloc[df_idx]
|
| 60 |
+
ground_truth = row["answer"]
|
| 61 |
+
|
| 62 |
+
# Call task-specific evaluation function
|
| 63 |
+
try:
|
| 64 |
+
reward = self._compute_reward(task_name, user_answer, ground_truth)
|
| 65 |
+
return float(reward)
|
| 66 |
+
|
| 67 |
+
except Exception as e:
|
| 68 |
+
# Preserve original traceback context for easier debugging
|
| 69 |
+
raise RuntimeError(f"Error computing reward for {task_name} instance {task_instance_id}: {e}") from e
|
| 70 |
+
|
| 71 |
+
def _compute_reward(self, task_name: str, user_answer: str, ground_truth: str) -> float:
|
| 72 |
+
"""Compute reward using task-specific logic"""
|
| 73 |
+
|
| 74 |
+
if task_name == "crispr_delivery":
|
| 75 |
+
# CRISPR expects answer as a letter (a-f), exact match
|
| 76 |
+
return 1.0 if user_answer.strip().lower() == ground_truth.strip().lower() else 0.0
|
| 77 |
+
|
| 78 |
+
elif task_name.startswith("gwas_causal_gene"):
|
| 79 |
+
# GWAS causal gene expects exact gene match (case-insensitive)
|
| 80 |
+
return 1.0 if user_answer.strip().upper() == ground_truth.strip().upper() else 0.0
|
| 81 |
+
|
| 82 |
+
elif task_name == "gwas_variant_prioritization":
|
| 83 |
+
# GWAS variant expects exact variant match
|
| 84 |
+
return 1.0 if user_answer.strip() == ground_truth.strip() else 0.0
|
| 85 |
+
|
| 86 |
+
elif task_name == "hle":
|
| 87 |
+
# HLE expects letter answer (A-Z), case-insensitive
|
| 88 |
+
return 1.0 if user_answer.strip().upper() == ground_truth.strip().upper() else 0.0
|
| 89 |
+
|
| 90 |
+
elif task_name.startswith("lab_bench"):
|
| 91 |
+
# Lab bench expects letter answer (A-Z), case-insensitive
|
| 92 |
+
return 1.0 if user_answer.strip().upper() == ground_truth.strip().upper() else 0.0
|
| 93 |
+
|
| 94 |
+
elif task_name == "rare_disease_diagnosis":
|
| 95 |
+
# Rare disease expects JSON with OMIM_ID match
|
| 96 |
+
# Parse both user answer and ground truth
|
| 97 |
+
try:
|
| 98 |
+
if isinstance(user_answer, str):
|
| 99 |
+
try:
|
| 100 |
+
user_dict = json.loads(user_answer)
|
| 101 |
+
except json.JSONDecodeError:
|
| 102 |
+
import ast
|
| 103 |
+
|
| 104 |
+
user_dict = ast.literal_eval(user_answer)
|
| 105 |
+
else:
|
| 106 |
+
user_dict = user_answer
|
| 107 |
+
|
| 108 |
+
if isinstance(ground_truth, str):
|
| 109 |
+
gt_dict = json.loads(ground_truth)
|
| 110 |
+
else:
|
| 111 |
+
gt_dict = ground_truth
|
| 112 |
+
|
| 113 |
+
# Compare OMIM_ID
|
| 114 |
+
return 1.0 if user_dict.get("OMIM_ID") == gt_dict.get("OMIM_ID") else 0.0
|
| 115 |
+
|
| 116 |
+
except Exception:
|
| 117 |
+
return 0.0
|
| 118 |
+
|
| 119 |
+
elif task_name == "screen_gene_retrieval":
|
| 120 |
+
# Screen gene retrieval expects gene symbol (case-insensitive)
|
| 121 |
+
return 1.0 if user_answer.strip().upper() == ground_truth.strip().upper() else 0.0
|
| 122 |
+
|
| 123 |
+
elif task_name == "patient_gene_detection":
|
| 124 |
+
# Patient gene detection expects JSON with causal_gene list
|
| 125 |
+
# Ground truth is a comma-separated string or single gene ID
|
| 126 |
+
try:
|
| 127 |
+
if isinstance(user_answer, str):
|
| 128 |
+
try:
|
| 129 |
+
user_dict = json.loads(user_answer)
|
| 130 |
+
except json.JSONDecodeError:
|
| 131 |
+
import ast
|
| 132 |
+
|
| 133 |
+
user_dict = ast.literal_eval(user_answer)
|
| 134 |
+
else:
|
| 135 |
+
user_dict = user_answer
|
| 136 |
+
|
| 137 |
+
# Get predicted genes
|
| 138 |
+
predicted_genes = user_dict.get("causal_gene", [])
|
| 139 |
+
if not isinstance(predicted_genes, list):
|
| 140 |
+
predicted_genes = [predicted_genes]
|
| 141 |
+
|
| 142 |
+
# Get ground truth genes (stored as comma-separated or single)
|
| 143 |
+
if "," in ground_truth:
|
| 144 |
+
true_genes = [g.strip() for g in ground_truth.split(",")]
|
| 145 |
+
else:
|
| 146 |
+
true_genes = [ground_truth]
|
| 147 |
+
|
| 148 |
+
# Check for intersection
|
| 149 |
+
if predicted_genes and set(true_genes) & set(predicted_genes):
|
| 150 |
+
return 1.0
|
| 151 |
+
else:
|
| 152 |
+
return 0.0
|
| 153 |
+
|
| 154 |
+
except Exception:
|
| 155 |
+
return 0.0
|
| 156 |
+
|
| 157 |
+
else:
|
| 158 |
+
raise ValueError(f"Unknown task: {task_name}")
|
| 159 |
+
|
| 160 |
+
def get_instance(self, task_name: str, task_instance_id: int) -> dict[str, Any]:
|
| 161 |
+
"""
|
| 162 |
+
Get information about a specific instance
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
task_name: Name of the task
|
| 166 |
+
task_instance_id: Task-specific instance ID
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
dict: Instance information including prompt, answer, etc.
|
| 170 |
+
"""
|
| 171 |
+
key = (task_name, task_instance_id)
|
| 172 |
+
if key not in self.instance_map:
|
| 173 |
+
raise ValueError(f"Instance not found: task={task_name}, task_instance_id={task_instance_id}")
|
| 174 |
+
|
| 175 |
+
df_idx = self.instance_map[key]
|
| 176 |
+
row = self.df.iloc[df_idx]
|
| 177 |
+
|
| 178 |
+
return {
|
| 179 |
+
"global_instance_id": row["instance_id"],
|
| 180 |
+
"task_instance_id": row["task_instance_id"],
|
| 181 |
+
"task_name": row["task_name"],
|
| 182 |
+
"split": row["split"],
|
| 183 |
+
"prompt": row["prompt"],
|
| 184 |
+
"answer": row["answer"],
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
def list_tasks(self) -> list:
|
| 188 |
+
"""Get list of all available tasks"""
|
| 189 |
+
return sorted(self.df["task_name"].unique().tolist())
|
| 190 |
+
|
| 191 |
+
def get_task_stats(self, task_name: str = None) -> dict[str, Any]:
|
| 192 |
+
"""
|
| 193 |
+
Get statistics for a task or all tasks
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
task_name: Optional task name to filter by
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
dict: Statistics including counts by split
|
| 200 |
+
"""
|
| 201 |
+
if task_name:
|
| 202 |
+
task_df = self.df[self.df["task_name"] == task_name]
|
| 203 |
+
if len(task_df) == 0:
|
| 204 |
+
raise ValueError(f"Task not found: {task_name}")
|
| 205 |
+
else:
|
| 206 |
+
task_df = self.df
|
| 207 |
+
|
| 208 |
+
stats = {
|
| 209 |
+
"total_instances": len(task_df),
|
| 210 |
+
"train_instances": len(task_df[task_df["split"] == "train"]),
|
| 211 |
+
"val_instances": len(task_df[task_df["split"] == "val"]),
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
if not task_name:
|
| 215 |
+
stats["tasks"] = {}
|
| 216 |
+
for tn in self.list_tasks():
|
| 217 |
+
stats["tasks"][tn] = self.get_task_stats(tn)
|
| 218 |
+
|
| 219 |
+
return stats
|
| 220 |
+
|
| 221 |
+
def batch_evaluate(self, evaluations: list) -> list:
|
| 222 |
+
"""
|
| 223 |
+
Evaluate multiple instances at once
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
evaluations: List of tuples (task_name, task_instance_id, user_answer)
|
| 227 |
+
|
| 228 |
+
Returns:
|
| 229 |
+
list: List of reward scores
|
| 230 |
+
"""
|
| 231 |
+
results = []
|
| 232 |
+
for task_name, task_instance_id, user_answer in evaluations:
|
| 233 |
+
try:
|
| 234 |
+
score = self.evaluate(task_name, task_instance_id, user_answer)
|
| 235 |
+
results.append(score)
|
| 236 |
+
except Exception as e:
|
| 237 |
+
print(f"Error evaluating {task_name} instance {task_instance_id}: {e}")
|
| 238 |
+
results.append(0.0)
|
| 239 |
+
|
| 240 |
+
return results
|
| 241 |
+
|
| 242 |
+
def get_instances_by_task(self, task_name: str, split: str = None) -> pd.DataFrame:
|
| 243 |
+
"""
|
| 244 |
+
Get all instances for a specific task
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
task_name: Name of the task
|
| 248 |
+
split: Optional split filter ('train' or 'val')
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
DataFrame with instances
|
| 252 |
+
"""
|
| 253 |
+
task_df = self.df[self.df["task_name"] == task_name]
|
| 254 |
+
|
| 255 |
+
if split:
|
| 256 |
+
task_df = task_df[task_df["split"] == split]
|
| 257 |
+
|
| 258 |
+
return task_df.copy()
|
| 259 |
+
|
| 260 |
+
def __repr__(self):
|
| 261 |
+
return f"BiomniEval1(instances={len(self.df)}, tasks={self.df['task_name'].nunique()})"
|
| 262 |
+
|
| 263 |
+
def __len__(self):
|
| 264 |
+
return len(self.df)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def main():
|
| 268 |
+
"""Demo usage of BiomniEval1"""
|
| 269 |
+
evaluator = BiomniEval1()
|
| 270 |
+
|
| 271 |
+
print("\nAvailable tasks:")
|
| 272 |
+
for task in evaluator.list_tasks():
|
| 273 |
+
print(f" - {task}")
|
| 274 |
+
|
| 275 |
+
print("\nOverall statistics:")
|
| 276 |
+
stats = evaluator.get_task_stats()
|
| 277 |
+
print(f" Total instances: {stats['total_instances']}")
|
| 278 |
+
print(f" Train: {stats['train_instances']}, Val: {stats['val_instances']}")
|
| 279 |
+
|
| 280 |
+
print("\nPer-task statistics:")
|
| 281 |
+
for task_name in evaluator.list_tasks():
|
| 282 |
+
task_stats = evaluator.get_task_stats(task_name)
|
| 283 |
+
print(
|
| 284 |
+
f" {task_name}: {task_stats['total_instances']} total ({task_stats['train_instances']} train, {task_stats['val_instances']} val)"
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
# Example evaluation
|
| 288 |
+
print("\n" + "=" * 60)
|
| 289 |
+
print("Example evaluation:")
|
| 290 |
+
print("=" * 60)
|
| 291 |
+
|
| 292 |
+
# Get first instance from gwas_variant_prioritization
|
| 293 |
+
first_instance = evaluator.df[evaluator.df["task_name"] == "gwas_variant_prioritization"].iloc[0]
|
| 294 |
+
task_name = first_instance["task_name"]
|
| 295 |
+
task_instance_id = first_instance["task_instance_id"]
|
| 296 |
+
ground_truth = first_instance["answer"]
|
| 297 |
+
|
| 298 |
+
print(f"\nTask: {task_name}")
|
| 299 |
+
print(f"Task Instance ID: {task_instance_id}")
|
| 300 |
+
print(f"Ground truth: {ground_truth}")
|
| 301 |
+
print(f"Prompt preview: {first_instance['prompt'][:200]}...")
|
| 302 |
+
|
| 303 |
+
# Test with correct answer
|
| 304 |
+
score = evaluator.evaluate(task_name, task_instance_id, ground_truth)
|
| 305 |
+
print(f"\nScore (correct answer '{ground_truth}'): {score}")
|
| 306 |
+
|
| 307 |
+
# Test with wrong answer
|
| 308 |
+
score = evaluator.evaluate(task_name, task_instance_id, "wrong_answer")
|
| 309 |
+
print(f"Score (wrong answer 'wrong_answer'): {score}")
|
| 310 |
+
|
| 311 |
+
# Batch evaluation example
|
| 312 |
+
print("\n" + "=" * 60)
|
| 313 |
+
print("Batch evaluation example:")
|
| 314 |
+
print("=" * 60)
|
| 315 |
+
batch_evals = [
|
| 316 |
+
(task_name, task_instance_id, ground_truth), # Correct
|
| 317 |
+
(task_name, task_instance_id, "wrong"), # Wrong
|
| 318 |
+
]
|
| 319 |
+
scores = evaluator.batch_evaluate(batch_evals)
|
| 320 |
+
print(f"Batch scores: {scores}")
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
if __name__ == "__main__":
|
| 324 |
+
main()
|
Biomanus_upload/biomni/graph/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from biomni.graph.graph_rag import MCPGraphRAGRetriever
|
| 2 |
+
from biomni.graph.graph_router import GraphRouter
|
| 3 |
+
from biomni.graph.planning_agent import GraphPlanningAgent
|
| 4 |
+
from biomni.graph.schema_extractor import ToolSchemaExtractor
|
| 5 |
+
from biomni.graph.tool_graph import ToolGraph
|
| 6 |
+
from biomni.graph.workflow_priors import WORKFLOW_PRIORS, WorkflowPrior, rank_workflow_priors
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"GraphPlanningAgent",
|
| 10 |
+
"GraphRouter",
|
| 11 |
+
"MCPGraphRAGRetriever",
|
| 12 |
+
"ToolSchemaExtractor",
|
| 13 |
+
"ToolGraph",
|
| 14 |
+
"WORKFLOW_PRIORS",
|
| 15 |
+
"WorkflowPrior",
|
| 16 |
+
"rank_workflow_priors",
|
| 17 |
+
]
|
Biomanus_upload/biomni/graph/graph_rag.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
from biomni.graph.tool_graph import ToolGraph
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class MCPGraphRAGRetriever:
|
| 9 |
+
"""Generic GraphRAG retriever over the full MCP tool graph.
|
| 10 |
+
|
| 11 |
+
This class intentionally avoids benchmark- or task-specific allow/deny
|
| 12 |
+
policies. It only retrieves a compact graph context from semantic overlap
|
| 13 |
+
between the query and graph nodes; the agentic planner decides how to use
|
| 14 |
+
the retrieved graph.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
TOKEN_PATTERN = re.compile(r"[a-z0-9_+-]+")
|
| 18 |
+
STAGE_RANK = {
|
| 19 |
+
"input_acquisition": 0,
|
| 20 |
+
"preprocessing": 1,
|
| 21 |
+
"analysis": 2,
|
| 22 |
+
"downstream": 3,
|
| 23 |
+
"reporting": 4,
|
| 24 |
+
}
|
| 25 |
+
GENERIC_TOKENS = {
|
| 26 |
+
"answer",
|
| 27 |
+
"based",
|
| 28 |
+
"biological",
|
| 29 |
+
"case",
|
| 30 |
+
"choice",
|
| 31 |
+
"choices",
|
| 32 |
+
"data",
|
| 33 |
+
"following",
|
| 34 |
+
"question",
|
| 35 |
+
"result",
|
| 36 |
+
"task",
|
| 37 |
+
"tool",
|
| 38 |
+
"using",
|
| 39 |
+
"which",
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
def retrieve(
|
| 43 |
+
self,
|
| 44 |
+
*,
|
| 45 |
+
query_context: dict,
|
| 46 |
+
query_semantics: dict,
|
| 47 |
+
tool_graph: ToolGraph,
|
| 48 |
+
candidate_pool: int = 24,
|
| 49 |
+
top_k_tools: int = 12,
|
| 50 |
+
) -> dict:
|
| 51 |
+
query_tokens = self._query_tokens(query_context, query_semantics)
|
| 52 |
+
query_operations = set(query_semantics.get("operations", []))
|
| 53 |
+
query_capabilities = set(query_semantics.get("capabilities", []))
|
| 54 |
+
query_datatypes = set(query_semantics.get("datatypes", []))
|
| 55 |
+
query_stages = set(query_semantics.get("stages", []))
|
| 56 |
+
|
| 57 |
+
candidates = []
|
| 58 |
+
for server_name, payload in tool_graph.server_index.items():
|
| 59 |
+
server_entry = tool_graph.get_server_entry(server_name) or {}
|
| 60 |
+
server_text = self._server_text(server_name, server_entry, payload)
|
| 61 |
+
server_tokens = self._tokenize(server_text)
|
| 62 |
+
server_overlap = query_tokens & server_tokens
|
| 63 |
+
|
| 64 |
+
for tool in payload.get("tools", []):
|
| 65 |
+
score, reasons = self._score_tool(
|
| 66 |
+
server_name=server_name,
|
| 67 |
+
server_tokens=server_tokens,
|
| 68 |
+
server_overlap=server_overlap,
|
| 69 |
+
tool=tool,
|
| 70 |
+
query_tokens=query_tokens,
|
| 71 |
+
query_operations=query_operations,
|
| 72 |
+
query_capabilities=query_capabilities,
|
| 73 |
+
query_datatypes=query_datatypes,
|
| 74 |
+
query_stages=query_stages,
|
| 75 |
+
tool_graph=tool_graph,
|
| 76 |
+
)
|
| 77 |
+
if score <= 0:
|
| 78 |
+
continue
|
| 79 |
+
candidates.append(
|
| 80 |
+
{
|
| 81 |
+
"server": server_name,
|
| 82 |
+
"tool": tool.get("name"),
|
| 83 |
+
"node_id": tool.get("node_id"),
|
| 84 |
+
"stage": tool.get("stage") or "analysis",
|
| 85 |
+
"operations": tool.get("operations", []),
|
| 86 |
+
"capabilities": tool.get("capabilities", []),
|
| 87 |
+
"consumes": tool.get("consumes", []),
|
| 88 |
+
"produces": tool.get("produces", []),
|
| 89 |
+
"score": round(score, 3),
|
| 90 |
+
"reason": "; ".join(reasons[:6]),
|
| 91 |
+
"graph_context": self._expand_tool_context(tool, tool_graph),
|
| 92 |
+
}
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
candidates.sort(key=lambda item: (item["score"], item["server"], item.get("tool") or ""), reverse=True)
|
| 96 |
+
shortlist = candidates[: max(candidate_pool, top_k_tools)]
|
| 97 |
+
compact = self._compact_subgraph(shortlist[:top_k_tools], tool_graph)
|
| 98 |
+
return {
|
| 99 |
+
"retriever": "mcp_graph_rag",
|
| 100 |
+
"task_profile": {
|
| 101 |
+
"categories": [str(item).lower() for item in query_context.get("categories", [])],
|
| 102 |
+
"operations": sorted(query_operations),
|
| 103 |
+
"capabilities": sorted(query_capabilities),
|
| 104 |
+
"datatypes": sorted(query_datatypes),
|
| 105 |
+
"stages": sorted(query_stages),
|
| 106 |
+
},
|
| 107 |
+
"query_tokens": sorted(query_tokens),
|
| 108 |
+
"candidate_tools": shortlist,
|
| 109 |
+
"compact_subgraph": compact,
|
| 110 |
+
"excluded_servers": [],
|
| 111 |
+
"operation_path_hint": self._operation_path_hint(shortlist[:top_k_tools]),
|
| 112 |
+
"context_text": self._format_context(shortlist[:top_k_tools], compact),
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
def _query_tokens(self, query_context: dict, query_semantics: dict) -> set[str]:
|
| 116 |
+
parts = [
|
| 117 |
+
query_context.get("question", ""),
|
| 118 |
+
query_context.get("original_query", ""),
|
| 119 |
+
query_context.get("rewritten_query", ""),
|
| 120 |
+
query_context.get("retrieval_query", ""),
|
| 121 |
+
" ".join(query_context.get("categories", [])),
|
| 122 |
+
" ".join(query_semantics.get("keywords", [])),
|
| 123 |
+
" ".join(query_semantics.get("operations", [])),
|
| 124 |
+
" ".join(query_semantics.get("capabilities", [])),
|
| 125 |
+
" ".join(query_semantics.get("datatypes", [])),
|
| 126 |
+
]
|
| 127 |
+
tokens = self._tokenize(" ".join(str(part) for part in parts if part))
|
| 128 |
+
return {token for token in tokens if token not in self.GENERIC_TOKENS and len(token) >= 3}
|
| 129 |
+
|
| 130 |
+
def _score_tool(
|
| 131 |
+
self,
|
| 132 |
+
*,
|
| 133 |
+
server_name: str,
|
| 134 |
+
server_tokens: set[str],
|
| 135 |
+
server_overlap: set[str],
|
| 136 |
+
tool: dict,
|
| 137 |
+
query_tokens: set[str],
|
| 138 |
+
query_operations: set[str],
|
| 139 |
+
query_capabilities: set[str],
|
| 140 |
+
query_datatypes: set[str],
|
| 141 |
+
query_stages: set[str],
|
| 142 |
+
tool_graph: ToolGraph,
|
| 143 |
+
) -> tuple[float, list[str]]:
|
| 144 |
+
text = " ".join(
|
| 145 |
+
[
|
| 146 |
+
server_name,
|
| 147 |
+
str(tool.get("name", "")),
|
| 148 |
+
str(tool.get("description", "")),
|
| 149 |
+
" ".join(tool.get("keywords", [])),
|
| 150 |
+
" ".join(tool.get("operations", [])),
|
| 151 |
+
" ".join(tool.get("capabilities", [])),
|
| 152 |
+
" ".join(tool.get("consumes", [])),
|
| 153 |
+
" ".join(tool.get("produces", [])),
|
| 154 |
+
]
|
| 155 |
+
).lower()
|
| 156 |
+
tool_tokens = self._tokenize(text)
|
| 157 |
+
score = 0.0
|
| 158 |
+
reasons = []
|
| 159 |
+
|
| 160 |
+
tool_overlap = query_tokens & tool_tokens
|
| 161 |
+
if tool_overlap:
|
| 162 |
+
score += min(len(tool_overlap), 10) * 0.8
|
| 163 |
+
reasons.append("tool_token_overlap=" + ",".join(sorted(list(tool_overlap))[:6]))
|
| 164 |
+
if server_overlap:
|
| 165 |
+
score += min(len(server_overlap), 6) * 0.35
|
| 166 |
+
reasons.append("server_token_overlap=" + ",".join(sorted(list(server_overlap))[:4]))
|
| 167 |
+
|
| 168 |
+
tool_operations = set(tool.get("operations", []))
|
| 169 |
+
operation_overlap = query_operations & tool_operations
|
| 170 |
+
if operation_overlap:
|
| 171 |
+
score += len(operation_overlap) * 4.0
|
| 172 |
+
score += len(operation_overlap) * (1.5 / max(1, len(tool_operations)))
|
| 173 |
+
reasons.append("operation_overlap=" + ",".join(sorted(operation_overlap)))
|
| 174 |
+
if len(tool_operations) > 3:
|
| 175 |
+
score -= min(len(tool_operations) - 3, 6) * 0.35
|
| 176 |
+
reasons.append("broad_operation_penalty")
|
| 177 |
+
|
| 178 |
+
capability_overlap = query_capabilities & set(tool.get("capabilities", []))
|
| 179 |
+
if capability_overlap:
|
| 180 |
+
score += len(capability_overlap) * 2.5
|
| 181 |
+
reasons.append("capability_overlap=" + ",".join(sorted(capability_overlap)))
|
| 182 |
+
|
| 183 |
+
tool_datatypes = set(tool.get("consumes", [])) | set(tool.get("produces", []))
|
| 184 |
+
datatype_overlap = query_datatypes & tool_datatypes
|
| 185 |
+
if datatype_overlap:
|
| 186 |
+
score += len(datatype_overlap) * 2.0
|
| 187 |
+
reasons.append("datatype_overlap=" + ",".join(sorted(datatype_overlap)))
|
| 188 |
+
|
| 189 |
+
stage = tool.get("stage")
|
| 190 |
+
if stage and stage in query_stages:
|
| 191 |
+
score += 1.0
|
| 192 |
+
reasons.append("stage_overlap=" + stage)
|
| 193 |
+
|
| 194 |
+
graph_bonus = self._graph_relevance_bonus(tool, query_operations, query_capabilities, query_datatypes, tool_graph)
|
| 195 |
+
if graph_bonus:
|
| 196 |
+
score += graph_bonus
|
| 197 |
+
reasons.append("graph_neighbor_overlap")
|
| 198 |
+
|
| 199 |
+
if not reasons and query_tokens & server_tokens:
|
| 200 |
+
score += 0.5
|
| 201 |
+
reasons.append("weak_server_match")
|
| 202 |
+
|
| 203 |
+
return score, reasons
|
| 204 |
+
|
| 205 |
+
def _graph_relevance_bonus(
|
| 206 |
+
self,
|
| 207 |
+
tool: dict,
|
| 208 |
+
query_operations: set[str],
|
| 209 |
+
query_capabilities: set[str],
|
| 210 |
+
query_datatypes: set[str],
|
| 211 |
+
tool_graph: ToolGraph,
|
| 212 |
+
) -> float:
|
| 213 |
+
node_id = tool.get("node_id")
|
| 214 |
+
if not node_id:
|
| 215 |
+
return 0.0
|
| 216 |
+
bonus = 0.0
|
| 217 |
+
for edge in tool_graph.adjacency.get(node_id, []):
|
| 218 |
+
target = tool_graph.nodes.get(edge.get("target"), {})
|
| 219 |
+
label = str(target.get("label", "")).lower()
|
| 220 |
+
node_type = target.get("type")
|
| 221 |
+
if node_type == "operation" and label in query_operations:
|
| 222 |
+
bonus += 2.0
|
| 223 |
+
elif node_type == "capability" and label in query_capabilities:
|
| 224 |
+
bonus += 1.5
|
| 225 |
+
elif node_type == "datatype" and label in query_datatypes:
|
| 226 |
+
bonus += 1.0
|
| 227 |
+
return min(bonus, 5.0)
|
| 228 |
+
|
| 229 |
+
def _compact_subgraph(self, candidates: list[dict], tool_graph: ToolGraph) -> dict:
|
| 230 |
+
nodes = {}
|
| 231 |
+
edges = []
|
| 232 |
+
candidate_node_ids = {item.get("node_id") for item in candidates if item.get("node_id")}
|
| 233 |
+
server_names = {item.get("server") for item in candidates if item.get("server")}
|
| 234 |
+
for server_name in server_names:
|
| 235 |
+
server_node = f"server:{server_name}"
|
| 236 |
+
if server_node in tool_graph.nodes:
|
| 237 |
+
nodes[server_node] = tool_graph.nodes[server_node]
|
| 238 |
+
for node_id in candidate_node_ids:
|
| 239 |
+
if node_id in tool_graph.nodes:
|
| 240 |
+
nodes[node_id] = tool_graph.nodes[node_id]
|
| 241 |
+
for edge in tool_graph.adjacency.get(node_id, []):
|
| 242 |
+
target = edge.get("target")
|
| 243 |
+
if target in tool_graph.nodes and tool_graph.nodes[target].get("type") in {
|
| 244 |
+
"operation",
|
| 245 |
+
"capability",
|
| 246 |
+
"datatype",
|
| 247 |
+
"stage",
|
| 248 |
+
"constraint",
|
| 249 |
+
}:
|
| 250 |
+
nodes[target] = tool_graph.nodes[target]
|
| 251 |
+
edges.append(edge)
|
| 252 |
+
return {"nodes": list(nodes.values()), "edges": edges[:120]}
|
| 253 |
+
|
| 254 |
+
def _expand_tool_context(self, tool: dict, tool_graph: ToolGraph) -> list[dict]:
|
| 255 |
+
node_id = tool.get("node_id")
|
| 256 |
+
if not node_id:
|
| 257 |
+
return []
|
| 258 |
+
context = []
|
| 259 |
+
for edge in tool_graph.adjacency.get(node_id, [])[:20]:
|
| 260 |
+
target = tool_graph.nodes.get(edge.get("target"), {})
|
| 261 |
+
if target:
|
| 262 |
+
context.append({"edge": edge.get("type"), "node_type": target.get("type"), "label": target.get("label")})
|
| 263 |
+
return context
|
| 264 |
+
|
| 265 |
+
def _operation_path_hint(self, candidates: list[dict]) -> list[str]:
|
| 266 |
+
operations = []
|
| 267 |
+
ordered = sorted(
|
| 268 |
+
candidates,
|
| 269 |
+
key=lambda item: (self.STAGE_RANK.get(item.get("stage"), 9), -float(item.get("score", 0.0))),
|
| 270 |
+
)
|
| 271 |
+
for item in ordered:
|
| 272 |
+
for operation in item.get("operations", []):
|
| 273 |
+
if operation not in operations:
|
| 274 |
+
operations.append(operation)
|
| 275 |
+
return operations[:8]
|
| 276 |
+
|
| 277 |
+
def _format_context(self, candidates: list[dict], compact: dict) -> str:
|
| 278 |
+
lines = [
|
| 279 |
+
f"Retrieved compact MCP graph: {len(compact.get('nodes', []))} nodes, {len(compact.get('edges', []))} edges",
|
| 280 |
+
"Top candidate tool bindings:",
|
| 281 |
+
]
|
| 282 |
+
for item in candidates[:12]:
|
| 283 |
+
lines.append(
|
| 284 |
+
"- "
|
| 285 |
+
+ f"{item['server']}.{item.get('tool')} score={item['score']} "
|
| 286 |
+
+ f"stage={item.get('stage')} ops={','.join(item.get('operations', [])[:4])}; {item.get('reason', '')}"
|
| 287 |
+
)
|
| 288 |
+
return "\n".join(lines)
|
| 289 |
+
|
| 290 |
+
def _server_text(self, server_name: str, server_entry: dict, payload: dict) -> str:
|
| 291 |
+
semantics = payload.get("semantics", {})
|
| 292 |
+
return " ".join(
|
| 293 |
+
[
|
| 294 |
+
server_name,
|
| 295 |
+
str(server_entry.get("summary", "")),
|
| 296 |
+
" ".join(server_entry.get("keywords", [])),
|
| 297 |
+
" ".join(semantics.get("keywords", [])),
|
| 298 |
+
" ".join(semantics.get("operations", [])),
|
| 299 |
+
" ".join(semantics.get("capabilities", [])),
|
| 300 |
+
" ".join(semantics.get("datatypes", [])),
|
| 301 |
+
]
|
| 302 |
+
).lower()
|
| 303 |
+
|
| 304 |
+
def _tokenize(self, text: str) -> set[str]:
|
| 305 |
+
normalized = (text or "").lower().replace("-", " ").replace("/", " ").replace("_", " ")
|
| 306 |
+
return {token for token in self.TOKEN_PATTERN.findall(normalized) if token}
|
Biomanus_upload/biomni/graph/graph_router.py
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import copy
|
| 4 |
+
from collections import deque
|
| 5 |
+
|
| 6 |
+
from biomni.graph.graph_rag import MCPGraphRAGRetriever
|
| 7 |
+
from biomni.graph.planning_agent import GraphPlanningAgent
|
| 8 |
+
from biomni.graph.schema_extractor import ToolSchemaExtractor
|
| 9 |
+
from biomni.graph.tool_graph import ToolGraph
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class GraphRouter:
|
| 13 |
+
"""First-pass GraphRAG router over the explicit tool graph."""
|
| 14 |
+
|
| 15 |
+
STAGE_ORDER = ["input_acquisition", "preprocessing", "analysis", "downstream", "reporting"]
|
| 16 |
+
|
| 17 |
+
def __init__(self, schema_extractor: ToolSchemaExtractor | None = None):
|
| 18 |
+
self.schema_extractor = schema_extractor or ToolSchemaExtractor()
|
| 19 |
+
self.planning_agent = GraphPlanningAgent(schema_extractor=self.schema_extractor)
|
| 20 |
+
self.graph_rag = MCPGraphRAGRetriever()
|
| 21 |
+
|
| 22 |
+
def route(
|
| 23 |
+
self,
|
| 24 |
+
query_context: dict,
|
| 25 |
+
tool_graph: ToolGraph,
|
| 26 |
+
*,
|
| 27 |
+
retriever=None,
|
| 28 |
+
llm=None,
|
| 29 |
+
top_k_servers: int = 8,
|
| 30 |
+
candidate_pool: int = 24,
|
| 31 |
+
top_k_tools: int = 12,
|
| 32 |
+
) -> dict:
|
| 33 |
+
if not tool_graph.get_server_names():
|
| 34 |
+
return {"selected_servers": [], "execution_plan": [], "seed_nodes": [], "workflow_subgraph": []}
|
| 35 |
+
|
| 36 |
+
query_semantics = self.schema_extractor.infer_query_semantics(query_context)
|
| 37 |
+
task_spec = query_semantics.get("task_spec") or {}
|
| 38 |
+
operation_anchors = query_semantics.get("operation_anchors", []) or task_spec.get("required_operations", [])
|
| 39 |
+
graph_rag_result = self.graph_rag.retrieve(
|
| 40 |
+
query_context=query_context,
|
| 41 |
+
query_semantics=query_semantics,
|
| 42 |
+
tool_graph=tool_graph,
|
| 43 |
+
candidate_pool=candidate_pool,
|
| 44 |
+
top_k_tools=top_k_tools,
|
| 45 |
+
)
|
| 46 |
+
if graph_rag_result.get("operation_path_hint"):
|
| 47 |
+
operation_anchors = self._merge_labels(operation_anchors, graph_rag_result["operation_path_hint"])
|
| 48 |
+
seed_nodes = self._find_seed_nodes(query_semantics, tool_graph)
|
| 49 |
+
candidate_paths = self._constrained_path_search(task_spec, operation_anchors, tool_graph)
|
| 50 |
+
ranked_paths = self._rank_paths(candidate_paths, task_spec, operation_anchors)
|
| 51 |
+
best_path = ranked_paths[0] if ranked_paths else self._anchor_only_path(operation_anchors, tool_graph)
|
| 52 |
+
heuristic_tool_plan = self._bind_tools_to_operations(
|
| 53 |
+
best_path,
|
| 54 |
+
task_spec,
|
| 55 |
+
query_semantics,
|
| 56 |
+
query_context,
|
| 57 |
+
tool_graph,
|
| 58 |
+
top_k_tools=top_k_tools,
|
| 59 |
+
)
|
| 60 |
+
planner_result = self.planning_agent.plan(
|
| 61 |
+
query_context=query_context,
|
| 62 |
+
query_semantics=query_semantics,
|
| 63 |
+
task_spec=task_spec,
|
| 64 |
+
ranked_paths=ranked_paths,
|
| 65 |
+
heuristic_tool_plan=heuristic_tool_plan,
|
| 66 |
+
tool_graph=tool_graph,
|
| 67 |
+
top_k_tools=top_k_tools,
|
| 68 |
+
llm=llm,
|
| 69 |
+
graph_rag_result=graph_rag_result,
|
| 70 |
+
)
|
| 71 |
+
tool_plan = planner_result.get("tool_plan") or heuristic_tool_plan
|
| 72 |
+
self._augment_graph_rag_with_tool_plan(graph_rag_result, tool_plan, tool_graph)
|
| 73 |
+
selected_servers = self._servers_from_tool_plan(tool_plan, tool_graph, top_k_servers=top_k_servers)
|
| 74 |
+
execution_plan = self._synthesize_dag(tool_plan)
|
| 75 |
+
operation_plan = self._build_operation_plan(
|
| 76 |
+
task_spec,
|
| 77 |
+
operation_anchors,
|
| 78 |
+
ranked_paths,
|
| 79 |
+
tool_plan,
|
| 80 |
+
planner_result=planner_result,
|
| 81 |
+
)
|
| 82 |
+
workflow_subgraph = [
|
| 83 |
+
{
|
| 84 |
+
"server": server["name"],
|
| 85 |
+
"tools": [tool.get("name", "") for tool in server.get("tools", [])],
|
| 86 |
+
"category": server.get("category", "general"),
|
| 87 |
+
"score": server.get("score", 0.0),
|
| 88 |
+
}
|
| 89 |
+
for server in selected_servers
|
| 90 |
+
]
|
| 91 |
+
return {
|
| 92 |
+
"task_spec": task_spec,
|
| 93 |
+
"operation_anchors": operation_anchors,
|
| 94 |
+
"candidate_paths": ranked_paths[:5],
|
| 95 |
+
"tool_plan": tool_plan,
|
| 96 |
+
"heuristic_tool_plan": heuristic_tool_plan,
|
| 97 |
+
"dag": execution_plan,
|
| 98 |
+
"heuristic_dag": self._synthesize_dag(heuristic_tool_plan),
|
| 99 |
+
"selected_servers": selected_servers,
|
| 100 |
+
"operation_plan": operation_plan,
|
| 101 |
+
"execution_plan": execution_plan,
|
| 102 |
+
"seed_nodes": seed_nodes,
|
| 103 |
+
"workflow_subgraph": workflow_subgraph,
|
| 104 |
+
"planning_agent": planner_result,
|
| 105 |
+
"graph_rag": graph_rag_result,
|
| 106 |
+
"compact_graph_rag_subgraph": graph_rag_result.get("compact_subgraph", {}),
|
| 107 |
+
"excluded_mcp_servers": graph_rag_result.get("excluded_servers", []),
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
def _merge_labels(self, primary: list[str], additions: list[str]) -> list[str]:
|
| 111 |
+
merged = list(primary or [])
|
| 112 |
+
for item in additions or []:
|
| 113 |
+
if item and item not in merged:
|
| 114 |
+
merged.append(item)
|
| 115 |
+
return merged
|
| 116 |
+
|
| 117 |
+
def _augment_graph_rag_with_tool_plan(self, graph_rag_result: dict, tool_plan: list[dict], tool_graph: ToolGraph) -> None:
|
| 118 |
+
"""Ensure the compact GraphRAG context contains the planner-bound graph tools."""
|
| 119 |
+
compact = graph_rag_result.setdefault("compact_subgraph", {"nodes": [], "edges": []})
|
| 120 |
+
nodes = compact.setdefault("nodes", [])
|
| 121 |
+
edges = compact.setdefault("edges", [])
|
| 122 |
+
node_ids = {node.get("id") for node in nodes}
|
| 123 |
+
edge_keys = {(edge.get("source"), edge.get("target"), edge.get("type")) for edge in edges}
|
| 124 |
+
|
| 125 |
+
def add_node(node_id: str) -> None:
|
| 126 |
+
if node_id in node_ids or node_id not in tool_graph.nodes:
|
| 127 |
+
return
|
| 128 |
+
nodes.append(tool_graph.nodes[node_id])
|
| 129 |
+
node_ids.add(node_id)
|
| 130 |
+
|
| 131 |
+
def add_edge(edge: dict) -> None:
|
| 132 |
+
key = (edge.get("source"), edge.get("target"), edge.get("type"))
|
| 133 |
+
if key in edge_keys:
|
| 134 |
+
return
|
| 135 |
+
edges.append(edge)
|
| 136 |
+
edge_keys.add(key)
|
| 137 |
+
|
| 138 |
+
for step in tool_plan:
|
| 139 |
+
if step.get("binding_source") == "agent":
|
| 140 |
+
continue
|
| 141 |
+
server = step.get("server")
|
| 142 |
+
tool = step.get("tool")
|
| 143 |
+
if not server or not tool:
|
| 144 |
+
continue
|
| 145 |
+
server_node = f"server:{server}"
|
| 146 |
+
tool_node = step.get("node_id") or f"tool:{server}:{tool}"
|
| 147 |
+
operation_node = f"operation:{step.get('operation')}"
|
| 148 |
+
add_node(server_node)
|
| 149 |
+
add_node(tool_node)
|
| 150 |
+
add_node(operation_node)
|
| 151 |
+
for edge in tool_graph.adjacency.get(server_node, []):
|
| 152 |
+
if edge.get("target") == tool_node:
|
| 153 |
+
add_edge(edge)
|
| 154 |
+
for edge in tool_graph.adjacency.get(tool_node, []):
|
| 155 |
+
target = edge.get("target")
|
| 156 |
+
if target == operation_node or (
|
| 157 |
+
target in tool_graph.nodes
|
| 158 |
+
and tool_graph.nodes[target].get("type") in {"operation", "capability", "datatype", "stage", "constraint"}
|
| 159 |
+
):
|
| 160 |
+
add_node(target)
|
| 161 |
+
add_edge(edge)
|
| 162 |
+
|
| 163 |
+
def _find_seed_nodes(self, query_semantics: dict, tool_graph: ToolGraph) -> list[str]:
|
| 164 |
+
seeds = []
|
| 165 |
+
for capability in query_semantics.get("capabilities", []):
|
| 166 |
+
node_id = f"capability:{capability}"
|
| 167 |
+
if node_id in tool_graph.nodes:
|
| 168 |
+
seeds.append(node_id)
|
| 169 |
+
for datatype in query_semantics.get("datatypes", []):
|
| 170 |
+
node_id = f"datatype:{datatype}"
|
| 171 |
+
if node_id in tool_graph.nodes:
|
| 172 |
+
seeds.append(node_id)
|
| 173 |
+
for stage in query_semantics.get("stages", []):
|
| 174 |
+
node_id = f"stage:{stage}"
|
| 175 |
+
if node_id in tool_graph.nodes:
|
| 176 |
+
seeds.append(node_id)
|
| 177 |
+
return seeds
|
| 178 |
+
|
| 179 |
+
def _score_server(self, server_name: str, query_context: dict, query_semantics: dict, tool_graph: ToolGraph) -> dict:
|
| 180 |
+
entry = tool_graph.get_server_entry(server_name) or {}
|
| 181 |
+
semantics = tool_graph.get_server_semantics(server_name)
|
| 182 |
+
score = 0.0
|
| 183 |
+
reasons = []
|
| 184 |
+
|
| 185 |
+
query_keywords = set(query_semantics.get("keywords", []))
|
| 186 |
+
server_keywords = set(semantics.get("keywords", []))
|
| 187 |
+
keyword_overlap = query_keywords & server_keywords
|
| 188 |
+
if keyword_overlap:
|
| 189 |
+
score += min(len(keyword_overlap), 8) * 0.6
|
| 190 |
+
reasons.append(f"keyword overlap={', '.join(sorted(list(keyword_overlap))[:6])}")
|
| 191 |
+
|
| 192 |
+
query_capabilities = set(query_semantics.get("capabilities", []))
|
| 193 |
+
server_capabilities = set(semantics.get("capabilities", []))
|
| 194 |
+
capability_overlap = query_capabilities & server_capabilities
|
| 195 |
+
if capability_overlap:
|
| 196 |
+
score += len(capability_overlap) * 3.0
|
| 197 |
+
reasons.append(f"capabilities={', '.join(sorted(capability_overlap))}")
|
| 198 |
+
|
| 199 |
+
query_datatypes = set(query_semantics.get("datatypes", []))
|
| 200 |
+
server_datatypes = set(semantics.get("datatypes", []))
|
| 201 |
+
datatype_overlap = query_datatypes & server_datatypes
|
| 202 |
+
if datatype_overlap:
|
| 203 |
+
score += len(datatype_overlap) * 2.0
|
| 204 |
+
reasons.append(f"datatypes={', '.join(sorted(datatype_overlap))}")
|
| 205 |
+
|
| 206 |
+
query_stages = set(query_semantics.get("stages", []))
|
| 207 |
+
server_stages = set(semantics.get("stages", []))
|
| 208 |
+
stage_overlap = query_stages & server_stages
|
| 209 |
+
if stage_overlap:
|
| 210 |
+
score += len(stage_overlap) * 1.5
|
| 211 |
+
reasons.append(f"stages={', '.join(sorted(stage_overlap))}")
|
| 212 |
+
|
| 213 |
+
for hint in query_context.get("server_hints", []):
|
| 214 |
+
hint_l = str(hint).lower()
|
| 215 |
+
if self._hint_matches_server(hint_l, server_name, entry):
|
| 216 |
+
score += 8.0
|
| 217 |
+
reasons.append(f"server_hint={hint_l}")
|
| 218 |
+
|
| 219 |
+
category = str(entry.get("category", "")).lower()
|
| 220 |
+
for category_hint in query_context.get("categories", []):
|
| 221 |
+
if str(category_hint).lower() == category:
|
| 222 |
+
score += 2.0
|
| 223 |
+
reasons.append(f"category={category}")
|
| 224 |
+
|
| 225 |
+
tool_bonus = min(len(tool_graph.get_tool_nodes_for_server(server_name)), 6) * 0.15
|
| 226 |
+
score += tool_bonus
|
| 227 |
+
|
| 228 |
+
return {
|
| 229 |
+
"server_name": server_name,
|
| 230 |
+
"server_entry": entry,
|
| 231 |
+
"score": score,
|
| 232 |
+
"reason": reasons,
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
def _select_tools_for_server(
|
| 236 |
+
self,
|
| 237 |
+
server_name: str,
|
| 238 |
+
query_context: dict,
|
| 239 |
+
query_semantics: dict,
|
| 240 |
+
tool_graph: ToolGraph,
|
| 241 |
+
*,
|
| 242 |
+
retriever=None,
|
| 243 |
+
llm=None,
|
| 244 |
+
top_k_tools: int = 12,
|
| 245 |
+
) -> list[dict]:
|
| 246 |
+
tools = tool_graph.get_tool_nodes_for_server(server_name)
|
| 247 |
+
if not tools:
|
| 248 |
+
return []
|
| 249 |
+
|
| 250 |
+
scored = []
|
| 251 |
+
query_keywords = set(query_semantics.get("keywords", []))
|
| 252 |
+
query_capabilities = set(query_semantics.get("capabilities", []))
|
| 253 |
+
query_datatypes = set(query_semantics.get("datatypes", []))
|
| 254 |
+
query_stages = set(query_semantics.get("stages", []))
|
| 255 |
+
|
| 256 |
+
for tool in tools:
|
| 257 |
+
score = 0.0
|
| 258 |
+
tool_keywords = set(tool.get("keywords", []))
|
| 259 |
+
capability_overlap = query_capabilities & set(tool.get("capabilities", []))
|
| 260 |
+
datatype_overlap = query_datatypes & (set(tool.get("consumes", [])) | set(tool.get("produces", [])))
|
| 261 |
+
stage_overlap = query_stages & {tool.get("stage")} if tool.get("stage") else set()
|
| 262 |
+
|
| 263 |
+
keyword_overlap = query_keywords & tool_keywords
|
| 264 |
+
score += min(len(keyword_overlap), 6) * 0.5
|
| 265 |
+
score += len(capability_overlap) * 3.0
|
| 266 |
+
score += len(datatype_overlap) * 2.0
|
| 267 |
+
score += len(stage_overlap) * 1.0
|
| 268 |
+
|
| 269 |
+
for hint in query_context.get("server_hints", []):
|
| 270 |
+
hint_l = str(hint).lower()
|
| 271 |
+
if self._hint_matches_tool(hint_l, tool):
|
| 272 |
+
score += 5.0
|
| 273 |
+
scored.append((score, tool))
|
| 274 |
+
|
| 275 |
+
scored.sort(key=lambda item: item[0], reverse=True)
|
| 276 |
+
shortlisted = [self._tool_to_runtime_schema(tool) for _, tool in scored[: max(top_k_tools * 2, top_k_tools)]]
|
| 277 |
+
|
| 278 |
+
if retriever is not None and llm is not None and len(shortlisted) > top_k_tools:
|
| 279 |
+
reranked = retriever._llm_rerank_tools(query_context, shortlisted, top_k=top_k_tools, llm=llm)
|
| 280 |
+
if reranked:
|
| 281 |
+
return reranked
|
| 282 |
+
return shortlisted[:top_k_tools]
|
| 283 |
+
|
| 284 |
+
def _hint_matches_server(self, hint: str, server_name: str, entry: dict) -> bool:
|
| 285 |
+
if len(hint) < 3:
|
| 286 |
+
return False
|
| 287 |
+
server_l = server_name.lower()
|
| 288 |
+
if hint == server_l or hint in server_l.split("-") or hint in server_l.split("_"):
|
| 289 |
+
return True
|
| 290 |
+
tokens = set(str(entry.get("summary", "")).lower().replace("_", "-").split())
|
| 291 |
+
return hint in tokens
|
| 292 |
+
|
| 293 |
+
def _hint_matches_tool(self, hint: str, tool: dict) -> bool:
|
| 294 |
+
if len(hint) < 3:
|
| 295 |
+
return False
|
| 296 |
+
tool_name = str(tool.get("name", "")).lower()
|
| 297 |
+
if hint == tool_name or hint in tool_name.replace("_", "-").split("-"):
|
| 298 |
+
return True
|
| 299 |
+
tokens = set(str(tool.get("text", "")).lower().replace("_", "-").split())
|
| 300 |
+
return hint in tokens
|
| 301 |
+
|
| 302 |
+
def _tool_to_runtime_schema(self, tool: dict) -> dict:
|
| 303 |
+
return {
|
| 304 |
+
"name": tool.get("name"),
|
| 305 |
+
"description": tool.get("description", ""),
|
| 306 |
+
"inputSchema": tool.get("inputSchema", {"properties": {}, "required": []}),
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
def _constrained_path_search(
|
| 310 |
+
self,
|
| 311 |
+
task_spec: dict,
|
| 312 |
+
operation_anchors: list[str],
|
| 313 |
+
tool_graph: ToolGraph,
|
| 314 |
+
*,
|
| 315 |
+
max_hops: int = 8,
|
| 316 |
+
beam_width: int = 80,
|
| 317 |
+
) -> list[dict]:
|
| 318 |
+
source_types = task_spec.get("input_types", []) or ["text"]
|
| 319 |
+
target_type = (task_spec.get("target_output") or {}).get("semantic_type")
|
| 320 |
+
constraint_labels = set((task_spec.get("constraints") or {}).get("labels", []))
|
| 321 |
+
reachable_anchors = [anchor for anchor in operation_anchors if anchor in tool_graph.operation_index]
|
| 322 |
+
paths = []
|
| 323 |
+
queue = deque((source_type, [], [source_type]) for source_type in source_types)
|
| 324 |
+
visited = set()
|
| 325 |
+
|
| 326 |
+
while queue and len(paths) < beam_width:
|
| 327 |
+
current_type, operations, type_trace = queue.popleft()
|
| 328 |
+
state = (current_type, tuple(operations))
|
| 329 |
+
if state in visited:
|
| 330 |
+
continue
|
| 331 |
+
visited.add(state)
|
| 332 |
+
|
| 333 |
+
if operations and (not target_type or current_type == target_type):
|
| 334 |
+
if self._path_satisfies_anchors(operations, reachable_anchors):
|
| 335 |
+
paths.append(self._path_payload(operations, type_trace, target_type, tool_graph))
|
| 336 |
+
continue
|
| 337 |
+
|
| 338 |
+
if len(operations) >= max_hops:
|
| 339 |
+
continue
|
| 340 |
+
|
| 341 |
+
for operation in tool_graph.datatype_to_operations.get(current_type, []):
|
| 342 |
+
if operation in operations:
|
| 343 |
+
continue
|
| 344 |
+
operation_info = tool_graph.operation_index.get(operation, {})
|
| 345 |
+
if not self._constraints_compatible(operation_info.get("constraints", []), constraint_labels):
|
| 346 |
+
continue
|
| 347 |
+
for produced_type in operation_info.get("produces", []) or []:
|
| 348 |
+
queue.append((produced_type, [*operations, operation], [*type_trace, produced_type]))
|
| 349 |
+
|
| 350 |
+
if not paths and reachable_anchors:
|
| 351 |
+
return [self._anchor_only_path(reachable_anchors, tool_graph)]
|
| 352 |
+
return paths
|
| 353 |
+
|
| 354 |
+
def _path_satisfies_anchors(self, operations: list[str], anchors: list[str]) -> bool:
|
| 355 |
+
return all(anchor in operations for anchor in anchors)
|
| 356 |
+
|
| 357 |
+
def _constraints_compatible(self, required: list[str], provided: set[str]) -> bool:
|
| 358 |
+
# Phase 1 routing is intentionally driven by the semantic type graph.
|
| 359 |
+
# Constraints are kept on operation/tool nodes for ranking and prompt
|
| 360 |
+
# explanation, but should not prune a path that can satisfy them via an
|
| 361 |
+
# upstream operation (for example gene_id_conversion -> kegg_enrichment).
|
| 362 |
+
return True
|
| 363 |
+
|
| 364 |
+
def _path_payload(
|
| 365 |
+
self,
|
| 366 |
+
operations: list[str],
|
| 367 |
+
type_trace: list[str],
|
| 368 |
+
target_type: str | None,
|
| 369 |
+
tool_graph: ToolGraph,
|
| 370 |
+
) -> dict:
|
| 371 |
+
steps = []
|
| 372 |
+
for index, operation in enumerate(operations):
|
| 373 |
+
operation_info = tool_graph.operation_index.get(operation, {})
|
| 374 |
+
steps.append(
|
| 375 |
+
{
|
| 376 |
+
"operation": operation,
|
| 377 |
+
"input_type": type_trace[index],
|
| 378 |
+
"output_types": operation_info.get("produces", []),
|
| 379 |
+
"stage": operation_info.get("stage", "analysis"),
|
| 380 |
+
}
|
| 381 |
+
)
|
| 382 |
+
return {
|
| 383 |
+
"operations": operations,
|
| 384 |
+
"type_trace": type_trace,
|
| 385 |
+
"target_type": target_type,
|
| 386 |
+
"steps": steps,
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
def _anchor_only_path(self, anchors: list[str], tool_graph: ToolGraph) -> dict:
|
| 390 |
+
operations = [operation for operation in anchors if operation in tool_graph.operation_index]
|
| 391 |
+
if not operations:
|
| 392 |
+
operations = list(tool_graph.operation_index.keys())[:1]
|
| 393 |
+
type_trace = []
|
| 394 |
+
for operation in operations:
|
| 395 |
+
operation_info = tool_graph.operation_index.get(operation, {})
|
| 396 |
+
if not type_trace:
|
| 397 |
+
type_trace.append((operation_info.get("accepts") or ["text"])[0])
|
| 398 |
+
type_trace.append((operation_info.get("produces") or [type_trace[-1]])[0])
|
| 399 |
+
return self._path_payload(operations, type_trace or ["text"], None, tool_graph)
|
| 400 |
+
|
| 401 |
+
def _rank_paths(self, paths: list[dict], task_spec: dict, anchors: list[str]) -> list[dict]:
|
| 402 |
+
target_type = (task_spec.get("target_output") or {}).get("semantic_type")
|
| 403 |
+
ranked = []
|
| 404 |
+
for path in paths:
|
| 405 |
+
operations = path.get("operations", [])
|
| 406 |
+
anchor_hits = len(set(operations) & set(anchors))
|
| 407 |
+
target_hit = 1 if target_type and target_type in path.get("type_trace", []) else 0
|
| 408 |
+
score = anchor_hits * 5.0 + target_hit * 3.0 - len(operations) * 0.2
|
| 409 |
+
enriched = dict(path)
|
| 410 |
+
enriched["score"] = score
|
| 411 |
+
enriched["anchor_hits"] = anchor_hits
|
| 412 |
+
ranked.append(enriched)
|
| 413 |
+
ranked.sort(key=lambda item: item.get("score", 0), reverse=True)
|
| 414 |
+
return ranked
|
| 415 |
+
|
| 416 |
+
def _bind_tools_to_operations(
|
| 417 |
+
self,
|
| 418 |
+
path: dict,
|
| 419 |
+
task_spec: dict,
|
| 420 |
+
query_semantics: dict,
|
| 421 |
+
query_context: dict,
|
| 422 |
+
tool_graph: ToolGraph,
|
| 423 |
+
*,
|
| 424 |
+
top_k_tools: int = 12,
|
| 425 |
+
) -> list[dict]:
|
| 426 |
+
constraint_labels = set((task_spec.get("constraints") or {}).get("labels", []))
|
| 427 |
+
query_keywords = {
|
| 428 |
+
keyword
|
| 429 |
+
for keyword in query_semantics.get("keywords", [])
|
| 430 |
+
if isinstance(keyword, str) and len(keyword) >= 3
|
| 431 |
+
}
|
| 432 |
+
generic_query_terms = {
|
| 433 |
+
"according",
|
| 434 |
+
"against",
|
| 435 |
+
"annotation",
|
| 436 |
+
"answer",
|
| 437 |
+
"biological",
|
| 438 |
+
"but",
|
| 439 |
+
"choices",
|
| 440 |
+
"data",
|
| 441 |
+
"database",
|
| 442 |
+
"entry",
|
| 443 |
+
"following",
|
| 444 |
+
"gene",
|
| 445 |
+
"genes",
|
| 446 |
+
"identify",
|
| 447 |
+
"knowledge",
|
| 448 |
+
"metadata",
|
| 449 |
+
"not",
|
| 450 |
+
"query",
|
| 451 |
+
"record",
|
| 452 |
+
"referenced",
|
| 453 |
+
"relevant",
|
| 454 |
+
"retrieve",
|
| 455 |
+
"retrieved",
|
| 456 |
+
"set",
|
| 457 |
+
"source",
|
| 458 |
+
"task",
|
| 459 |
+
"which",
|
| 460 |
+
}
|
| 461 |
+
discriminative_keywords = query_keywords - generic_query_terms
|
| 462 |
+
query_categories = {str(category).lower() for category in query_context.get("categories", [])}
|
| 463 |
+
tool_plan = []
|
| 464 |
+
for step_index, operation in enumerate(path.get("operations", []), start=1):
|
| 465 |
+
operation_info = tool_graph.operation_index.get(operation, {})
|
| 466 |
+
candidates = []
|
| 467 |
+
for tool in operation_info.get("tools", []):
|
| 468 |
+
server_name = tool.get("node_id", "tool:unknown:unknown").split(":")[1]
|
| 469 |
+
server_entry = tool_graph.get_server_entry(server_name) or {}
|
| 470 |
+
tool_constraints = set(tool.get("constraints", []))
|
| 471 |
+
tool_text = " ".join(
|
| 472 |
+
[
|
| 473 |
+
server_name,
|
| 474 |
+
str(server_entry.get("summary", "")),
|
| 475 |
+
str(tool.get("name", "")),
|
| 476 |
+
str(tool.get("description", "")),
|
| 477 |
+
" ".join(tool.get("keywords", [])),
|
| 478 |
+
]
|
| 479 |
+
).lower()
|
| 480 |
+
operation_spec = self.schema_extractor.OPERATION_SPECS.get(operation, {})
|
| 481 |
+
operation_patterns = operation_spec.get("patterns", set())
|
| 482 |
+
score = 1.0
|
| 483 |
+
score += len(tool_constraints & constraint_labels) * 2.0
|
| 484 |
+
score += min(len(tool.get("keywords", [])), 8) * 0.05
|
| 485 |
+
keyword_hits = {keyword for keyword in query_keywords if keyword in tool_text}
|
| 486 |
+
discriminative_hits = {keyword for keyword in discriminative_keywords if keyword in tool_text}
|
| 487 |
+
score += min(len(keyword_hits), 10) * 0.45
|
| 488 |
+
score += min(len(discriminative_hits), 6) * 1.2
|
| 489 |
+
category = str(server_entry.get("category", "")).lower()
|
| 490 |
+
if category and category in query_categories:
|
| 491 |
+
score += 1.0
|
| 492 |
+
if operation in tool.get("name", "").lower():
|
| 493 |
+
score += 1.0
|
| 494 |
+
if operation.replace("_", " ") in tool_text:
|
| 495 |
+
score += 1.5
|
| 496 |
+
score += min(sum(1 for pattern in operation_patterns if pattern in tool_text), 3) * 0.8
|
| 497 |
+
if operation == "csv_export":
|
| 498 |
+
tool_name = str(tool.get("name", "")).lower()
|
| 499 |
+
if "export" in tool_name:
|
| 500 |
+
score += 2.0
|
| 501 |
+
if "csv" in tool_name:
|
| 502 |
+
score += 1.0
|
| 503 |
+
if tool_name.startswith(("read", "load", "import", "fetch", "get")) or "_read_" in tool_name:
|
| 504 |
+
score -= 3.0
|
| 505 |
+
candidates.append((score, tool.get("node_id", ""), tool))
|
| 506 |
+
candidates.sort(key=lambda item: (item[0], item[1]), reverse=True)
|
| 507 |
+
selected = candidates[0][2] if candidates else {}
|
| 508 |
+
server_name = selected.get("node_id", "tool:unknown:unknown").split(":")[1]
|
| 509 |
+
tool_plan.append(
|
| 510 |
+
{
|
| 511 |
+
"step": step_index,
|
| 512 |
+
"operation": operation,
|
| 513 |
+
"server": server_name,
|
| 514 |
+
"tool": selected.get("name"),
|
| 515 |
+
"stage": operation_info.get("stage", "analysis"),
|
| 516 |
+
"input_types": operation_info.get("accepts", []),
|
| 517 |
+
"output_types": operation_info.get("produces", []),
|
| 518 |
+
"candidate_tool_count": len(candidates),
|
| 519 |
+
}
|
| 520 |
+
)
|
| 521 |
+
return tool_plan[:top_k_tools]
|
| 522 |
+
|
| 523 |
+
def _servers_from_tool_plan(self, tool_plan: list[dict], tool_graph: ToolGraph, *, top_k_servers: int) -> list[dict]:
|
| 524 |
+
grouped: dict[str, list[dict]] = {}
|
| 525 |
+
for step in tool_plan:
|
| 526 |
+
if step.get("binding_source") == "agent":
|
| 527 |
+
continue
|
| 528 |
+
if step.get("server") and step.get("tool"):
|
| 529 |
+
grouped.setdefault(step["server"], []).append(step)
|
| 530 |
+
|
| 531 |
+
selected_servers = []
|
| 532 |
+
for server_name, steps in list(grouped.items())[:top_k_servers]:
|
| 533 |
+
entry = copy.deepcopy(tool_graph.get_server_entry(server_name) or {"name": server_name})
|
| 534 |
+
entry["tools"] = [
|
| 535 |
+
{
|
| 536 |
+
"name": step["tool"],
|
| 537 |
+
"description": f"Tool bound for operation {step['operation']}",
|
| 538 |
+
"inputSchema": {"properties": {}, "required": []},
|
| 539 |
+
}
|
| 540 |
+
for step in steps
|
| 541 |
+
]
|
| 542 |
+
entry["score"] = float(len(steps))
|
| 543 |
+
entry["graph_reason"] = [f"operation_path_binding={step['operation']}" for step in steps]
|
| 544 |
+
selected_servers.append(entry)
|
| 545 |
+
return selected_servers
|
| 546 |
+
|
| 547 |
+
def _synthesize_dag(self, tool_plan: list[dict]) -> list[dict]:
|
| 548 |
+
dag = []
|
| 549 |
+
previous_id = None
|
| 550 |
+
seen_ids: dict[str, int] = {}
|
| 551 |
+
for index, step in enumerate(tool_plan, start=1):
|
| 552 |
+
base_id = f"{step['server']}.{step['tool'] or step['operation']}"
|
| 553 |
+
seen_ids[base_id] = seen_ids.get(base_id, 0) + 1
|
| 554 |
+
suffix = "" if seen_ids[base_id] == 1 else f"#{seen_ids[base_id]}"
|
| 555 |
+
step_id = f"{base_id}{suffix}"
|
| 556 |
+
dag.append(
|
| 557 |
+
{
|
| 558 |
+
"id": step_id,
|
| 559 |
+
"step": index,
|
| 560 |
+
"stage": step.get("stage", "analysis"),
|
| 561 |
+
"operation": step.get("operation"),
|
| 562 |
+
"server": step.get("server"),
|
| 563 |
+
"tools": [step.get("tool") or step.get("operation")],
|
| 564 |
+
"input_types": step.get("input_types", []),
|
| 565 |
+
"output_types": step.get("output_types", []),
|
| 566 |
+
"binding_source": step.get("binding_source", "graph"),
|
| 567 |
+
"depends_on": [previous_id] if previous_id else [],
|
| 568 |
+
}
|
| 569 |
+
)
|
| 570 |
+
previous_id = step_id
|
| 571 |
+
return dag
|
| 572 |
+
|
| 573 |
+
def _build_operation_plan(
|
| 574 |
+
self,
|
| 575 |
+
task_spec: dict,
|
| 576 |
+
operation_anchors: list[str],
|
| 577 |
+
ranked_paths: list[dict],
|
| 578 |
+
tool_plan: list[dict],
|
| 579 |
+
*,
|
| 580 |
+
planner_result: dict | None = None,
|
| 581 |
+
) -> dict:
|
| 582 |
+
best_path = ranked_paths[0] if ranked_paths else {}
|
| 583 |
+
selected_path = planner_result.get("operation_path") if planner_result else None
|
| 584 |
+
selected_trace = planner_result.get("type_trace") if planner_result else None
|
| 585 |
+
return {
|
| 586 |
+
"workflow_type": task_spec.get("workflow_type", "single_step"),
|
| 587 |
+
"planner": (planner_result or {}).get("planner", "heuristic_graph_router"),
|
| 588 |
+
"planner_selected": bool((planner_result or {}).get("selected")),
|
| 589 |
+
"task_spec": task_spec,
|
| 590 |
+
"operation_anchors": operation_anchors,
|
| 591 |
+
"operation_path": selected_path or best_path.get("operations", []),
|
| 592 |
+
"type_trace": selected_trace or best_path.get("type_trace", []),
|
| 593 |
+
"steps": [
|
| 594 |
+
{
|
| 595 |
+
"operation": step.get("operation"),
|
| 596 |
+
"server": step.get("server"),
|
| 597 |
+
"tool": step.get("tool"),
|
| 598 |
+
"input_types": step.get("input_types", []),
|
| 599 |
+
"output_types": step.get("output_types", []),
|
| 600 |
+
"binding_source": step.get("binding_source", "graph"),
|
| 601 |
+
"planning_rationale": step.get("planning_rationale", ""),
|
| 602 |
+
}
|
| 603 |
+
for step in tool_plan
|
| 604 |
+
],
|
| 605 |
+
"tool_bindings": [
|
| 606 |
+
{
|
| 607 |
+
"operation": step.get("operation"),
|
| 608 |
+
"server": step.get("server"),
|
| 609 |
+
"tool": step.get("tool"),
|
| 610 |
+
"binding_source": step.get("binding_source", "graph"),
|
| 611 |
+
}
|
| 612 |
+
for step in tool_plan
|
| 613 |
+
],
|
| 614 |
+
"planner_reason": (planner_result or {}).get("reason", ""),
|
| 615 |
+
"planner_task_profile": (planner_result or {}).get("task_profile", {}),
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
def _build_execution_plan(self, selected_servers: list[dict], tool_graph: ToolGraph) -> list[dict]:
|
| 619 |
+
selected_nodes = {}
|
| 620 |
+
for server in selected_servers:
|
| 621 |
+
selected_tool_names = {candidate.get("name") for candidate in server.get("tools", [])}
|
| 622 |
+
for tool in tool_graph.get_tool_nodes_for_server(server["name"]):
|
| 623 |
+
if tool.get("name") in selected_tool_names:
|
| 624 |
+
selected_nodes[tool["node_id"]] = tool
|
| 625 |
+
|
| 626 |
+
if selected_nodes:
|
| 627 |
+
dependency_map: dict[str, list[str]] = {node_id: [] for node_id in selected_nodes}
|
| 628 |
+
for edge in tool_graph.edges:
|
| 629 |
+
if edge.get("type") not in {"typed_flow", "follows"}:
|
| 630 |
+
continue
|
| 631 |
+
source = edge.get("source")
|
| 632 |
+
target = edge.get("target")
|
| 633 |
+
if source in selected_nodes and target in selected_nodes:
|
| 634 |
+
dependency_map[target].append(source)
|
| 635 |
+
|
| 636 |
+
stage_rank = {stage: idx for idx, stage in enumerate(self.STAGE_ORDER)}
|
| 637 |
+
ordered_nodes = sorted(
|
| 638 |
+
selected_nodes.values(),
|
| 639 |
+
key=lambda tool: (
|
| 640 |
+
stage_rank.get(tool.get("stage") or "analysis", 99),
|
| 641 |
+
len(dependency_map.get(tool["node_id"], [])),
|
| 642 |
+
tool["node_id"].split(":")[1],
|
| 643 |
+
tool.get("name", ""),
|
| 644 |
+
),
|
| 645 |
+
)
|
| 646 |
+
plan = []
|
| 647 |
+
for tool in ordered_nodes:
|
| 648 |
+
server = tool["node_id"].split(":")[1]
|
| 649 |
+
depends_on = [
|
| 650 |
+
f"{selected_nodes[source]['node_id'].split(':')[1]}.{selected_nodes[source].get('name')}"
|
| 651 |
+
for source in dependency_map.get(tool["node_id"], [])
|
| 652 |
+
]
|
| 653 |
+
plan.append(
|
| 654 |
+
{
|
| 655 |
+
"stage": tool.get("stage") or "analysis",
|
| 656 |
+
"server": server,
|
| 657 |
+
"tools": [tool.get("name")],
|
| 658 |
+
"depends_on": depends_on,
|
| 659 |
+
}
|
| 660 |
+
)
|
| 661 |
+
return plan
|
| 662 |
+
|
| 663 |
+
plan = []
|
| 664 |
+
for server in selected_servers:
|
| 665 |
+
stage_buckets: dict[str, list[str]] = {}
|
| 666 |
+
for tool in tool_graph.get_tool_nodes_for_server(server["name"]):
|
| 667 |
+
if tool.get("name") not in {candidate.get("name") for candidate in server.get("tools", [])}:
|
| 668 |
+
continue
|
| 669 |
+
stage = tool.get("stage") or "analysis"
|
| 670 |
+
stage_buckets.setdefault(stage, []).append(tool.get("name"))
|
| 671 |
+
for stage in self.STAGE_ORDER:
|
| 672 |
+
if stage in stage_buckets:
|
| 673 |
+
plan.append({"stage": stage, "server": server["name"], "tools": sorted(stage_buckets[stage])})
|
| 674 |
+
return plan
|
Biomanus_upload/biomni/graph/planning_agent.py
ADDED
|
@@ -0,0 +1,1046 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
from langchain_core.messages import HumanMessage
|
| 7 |
+
|
| 8 |
+
from biomni.graph.schema_extractor import ToolSchemaExtractor
|
| 9 |
+
from biomni.graph.tool_graph import ToolGraph
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class GraphPlanningAgent:
|
| 13 |
+
"""Task-aware planner that refines graph routing into a more usable call chain."""
|
| 14 |
+
|
| 15 |
+
GENERIC_TOKENS = {
|
| 16 |
+
"according",
|
| 17 |
+
"analysis",
|
| 18 |
+
"annotate",
|
| 19 |
+
"answer",
|
| 20 |
+
"based",
|
| 21 |
+
"biological",
|
| 22 |
+
"candidate",
|
| 23 |
+
"choice",
|
| 24 |
+
"choices",
|
| 25 |
+
"compare",
|
| 26 |
+
"comparison",
|
| 27 |
+
"context",
|
| 28 |
+
"data",
|
| 29 |
+
"database",
|
| 30 |
+
"evidence",
|
| 31 |
+
"following",
|
| 32 |
+
"identify",
|
| 33 |
+
"likely",
|
| 34 |
+
"most",
|
| 35 |
+
"prompt",
|
| 36 |
+
"question",
|
| 37 |
+
"record",
|
| 38 |
+
"relevant",
|
| 39 |
+
"result",
|
| 40 |
+
"sequence",
|
| 41 |
+
"source",
|
| 42 |
+
"summary",
|
| 43 |
+
"task",
|
| 44 |
+
"tool",
|
| 45 |
+
"using",
|
| 46 |
+
"variant",
|
| 47 |
+
"which",
|
| 48 |
+
}
|
| 49 |
+
AMINO_ACID_SEQUENCE = re.compile(r"[ACDEFGHIKLMNPQRSTVWY]{12,}")
|
| 50 |
+
TOKEN_PATTERN = re.compile(r"[a-z0-9_+-]+")
|
| 51 |
+
EDGE_TYPES = {"typed_flow", "follows", "adjacent_stage"}
|
| 52 |
+
RESOURCE_TOKENS = {
|
| 53 |
+
"clinvar",
|
| 54 |
+
"disgenet",
|
| 55 |
+
"omim",
|
| 56 |
+
"uniprot",
|
| 57 |
+
"ensembl",
|
| 58 |
+
"pubmed",
|
| 59 |
+
"biomart",
|
| 60 |
+
}
|
| 61 |
+
SEQUENCING_INPUT_TYPES = {"fastq", "bam", "reference_genome", "annotation_gff", "raw_count_matrix", "count_matrix"}
|
| 62 |
+
WRONG_DOMAIN_TOKENS = {
|
| 63 |
+
"crispr",
|
| 64 |
+
"amplicon",
|
| 65 |
+
"guide",
|
| 66 |
+
"single-cell",
|
| 67 |
+
"singlecell",
|
| 68 |
+
"spatial",
|
| 69 |
+
"metagenomic",
|
| 70 |
+
"metagenomics",
|
| 71 |
+
"taxonomy",
|
| 72 |
+
"taxonomic",
|
| 73 |
+
"promoter",
|
| 74 |
+
"orf",
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
def __init__(self, schema_extractor: ToolSchemaExtractor | None = None):
|
| 78 |
+
self.schema_extractor = schema_extractor or ToolSchemaExtractor()
|
| 79 |
+
|
| 80 |
+
def plan(
|
| 81 |
+
self,
|
| 82 |
+
*,
|
| 83 |
+
query_context: dict,
|
| 84 |
+
query_semantics: dict,
|
| 85 |
+
task_spec: dict,
|
| 86 |
+
ranked_paths: list[dict],
|
| 87 |
+
heuristic_tool_plan: list[dict],
|
| 88 |
+
tool_graph: ToolGraph,
|
| 89 |
+
top_k_tools: int = 12,
|
| 90 |
+
llm=None,
|
| 91 |
+
graph_rag_result: dict | None = None,
|
| 92 |
+
) -> dict:
|
| 93 |
+
profile = self._build_task_profile(query_context, query_semantics, task_spec)
|
| 94 |
+
if graph_rag_result:
|
| 95 |
+
profile["graph_rag"] = graph_rag_result.get("task_profile", {})
|
| 96 |
+
profile["task_tags"] = sorted(
|
| 97 |
+
set(profile.get("task_tags", [])) | set((graph_rag_result.get("task_profile") or {}).get("tags", []))
|
| 98 |
+
)
|
| 99 |
+
step_specs = self._build_step_specs(query_context, task_spec, ranked_paths, profile)
|
| 100 |
+
if llm is not None and graph_rag_result:
|
| 101 |
+
llm_plan = self._plan_with_llm(
|
| 102 |
+
llm=llm,
|
| 103 |
+
query_context=query_context,
|
| 104 |
+
query_semantics=query_semantics,
|
| 105 |
+
task_spec=task_spec,
|
| 106 |
+
graph_rag_result=graph_rag_result,
|
| 107 |
+
tool_graph=tool_graph,
|
| 108 |
+
top_k_tools=top_k_tools,
|
| 109 |
+
)
|
| 110 |
+
if llm_plan.get("tool_plan"):
|
| 111 |
+
return {
|
| 112 |
+
"planner": "llm_graph_rag_planning_agent",
|
| 113 |
+
"selected": True,
|
| 114 |
+
"task_profile": profile,
|
| 115 |
+
"step_specs": step_specs,
|
| 116 |
+
"llm_enabled": True,
|
| 117 |
+
"operation_path": [step["operation"] for step in llm_plan["tool_plan"]],
|
| 118 |
+
"type_trace": self._build_type_trace(llm_plan["tool_plan"], task_spec),
|
| 119 |
+
"tool_plan": llm_plan["tool_plan"][:top_k_tools],
|
| 120 |
+
"reason": llm_plan.get("reason", "LLM planned over compact MCP GraphRAG context"),
|
| 121 |
+
"llm_raw_plan": llm_plan.get("raw_plan", {}),
|
| 122 |
+
"heuristic_tool_plan": heuristic_tool_plan,
|
| 123 |
+
"graph_rag": graph_rag_result or {},
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
tool_plan = self._select_tool_chain(
|
| 127 |
+
step_specs,
|
| 128 |
+
profile,
|
| 129 |
+
tool_graph,
|
| 130 |
+
top_k_tools=top_k_tools,
|
| 131 |
+
graph_rag_candidates=(graph_rag_result or {}).get("candidate_tools", []),
|
| 132 |
+
)
|
| 133 |
+
if not tool_plan:
|
| 134 |
+
return {
|
| 135 |
+
"planner": "graph_planning_agent",
|
| 136 |
+
"selected": False,
|
| 137 |
+
"task_profile": profile,
|
| 138 |
+
"step_specs": step_specs,
|
| 139 |
+
"reason": "no viable planner tool chain found; using heuristic route",
|
| 140 |
+
"llm_enabled": llm is not None,
|
| 141 |
+
"graph_rag": graph_rag_result or {},
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
return {
|
| 145 |
+
"planner": "graph_planning_agent",
|
| 146 |
+
"selected": True,
|
| 147 |
+
"task_profile": profile,
|
| 148 |
+
"step_specs": step_specs,
|
| 149 |
+
"llm_enabled": llm is not None,
|
| 150 |
+
"operation_path": [step["operation"] for step in tool_plan],
|
| 151 |
+
"type_trace": self._build_type_trace(tool_plan, task_spec),
|
| 152 |
+
"tool_plan": tool_plan[:top_k_tools],
|
| 153 |
+
"reason": "planner selected a task-grounded tool chain from graph candidates",
|
| 154 |
+
"heuristic_tool_plan": heuristic_tool_plan,
|
| 155 |
+
"graph_rag": graph_rag_result or {},
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
def _plan_with_llm(
|
| 159 |
+
self,
|
| 160 |
+
*,
|
| 161 |
+
llm,
|
| 162 |
+
query_context: dict,
|
| 163 |
+
query_semantics: dict,
|
| 164 |
+
task_spec: dict,
|
| 165 |
+
graph_rag_result: dict,
|
| 166 |
+
tool_graph: ToolGraph,
|
| 167 |
+
top_k_tools: int,
|
| 168 |
+
) -> dict:
|
| 169 |
+
candidates = graph_rag_result.get("candidate_tools", [])[: max(top_k_tools * 2, top_k_tools)]
|
| 170 |
+
if not candidates:
|
| 171 |
+
return {}
|
| 172 |
+
|
| 173 |
+
prompt = self._build_llm_planner_prompt(
|
| 174 |
+
query_context=query_context,
|
| 175 |
+
query_semantics=query_semantics,
|
| 176 |
+
task_spec=task_spec,
|
| 177 |
+
graph_rag_result=graph_rag_result,
|
| 178 |
+
candidates=candidates,
|
| 179 |
+
top_k_tools=top_k_tools,
|
| 180 |
+
)
|
| 181 |
+
try:
|
| 182 |
+
response = llm.invoke([HumanMessage(content=prompt)])
|
| 183 |
+
except Exception as exc:
|
| 184 |
+
return {"error": f"llm_planner_failed: {type(exc).__name__}: {exc}"}
|
| 185 |
+
|
| 186 |
+
content = getattr(response, "content", response)
|
| 187 |
+
content_text = self._content_to_text(content)
|
| 188 |
+
parsed = self._parse_json_object(content_text)
|
| 189 |
+
if not parsed:
|
| 190 |
+
return {"error": "llm_planner_returned_unparseable_json", "content": content_text[:2000]}
|
| 191 |
+
|
| 192 |
+
tool_plan = self._tool_plan_from_llm_json(parsed, candidates, tool_graph, top_k_tools=top_k_tools)
|
| 193 |
+
if not tool_plan:
|
| 194 |
+
return {"raw_plan": parsed, "reason": parsed.get("reason", "")}
|
| 195 |
+
return {"tool_plan": tool_plan, "raw_plan": parsed, "reason": parsed.get("reason", "")}
|
| 196 |
+
|
| 197 |
+
def _build_llm_planner_prompt(
|
| 198 |
+
self,
|
| 199 |
+
*,
|
| 200 |
+
query_context: dict,
|
| 201 |
+
query_semantics: dict,
|
| 202 |
+
task_spec: dict,
|
| 203 |
+
graph_rag_result: dict,
|
| 204 |
+
candidates: list[dict],
|
| 205 |
+
top_k_tools: int,
|
| 206 |
+
) -> str:
|
| 207 |
+
compact_candidates = []
|
| 208 |
+
for idx, item in enumerate(candidates):
|
| 209 |
+
compact_candidates.append(
|
| 210 |
+
{
|
| 211 |
+
"index": idx,
|
| 212 |
+
"server": item.get("server"),
|
| 213 |
+
"tool": item.get("tool"),
|
| 214 |
+
"stage": item.get("stage"),
|
| 215 |
+
"operations": item.get("operations", []),
|
| 216 |
+
"capabilities": item.get("capabilities", []),
|
| 217 |
+
"consumes": item.get("consumes", []),
|
| 218 |
+
"produces": item.get("produces", []),
|
| 219 |
+
"score": item.get("score"),
|
| 220 |
+
"graph_context": item.get("graph_context", [])[:8],
|
| 221 |
+
"reason": item.get("reason", ""),
|
| 222 |
+
}
|
| 223 |
+
)
|
| 224 |
+
question = query_context.get("question") or query_context.get("original_query") or query_context.get("rewritten_query")
|
| 225 |
+
payload = {
|
| 226 |
+
"question": question,
|
| 227 |
+
"query_semantics": {
|
| 228 |
+
"operations": query_semantics.get("operations", []),
|
| 229 |
+
"capabilities": query_semantics.get("capabilities", []),
|
| 230 |
+
"datatypes": query_semantics.get("datatypes", []),
|
| 231 |
+
"stages": query_semantics.get("stages", []),
|
| 232 |
+
},
|
| 233 |
+
"task_spec": task_spec,
|
| 234 |
+
"graph_rag": {
|
| 235 |
+
"operation_path_hint": graph_rag_result.get("operation_path_hint", []),
|
| 236 |
+
"compact_node_count": len((graph_rag_result.get("compact_subgraph") or {}).get("nodes", [])),
|
| 237 |
+
"compact_edge_count": len((graph_rag_result.get("compact_subgraph") or {}).get("edges", [])),
|
| 238 |
+
},
|
| 239 |
+
"candidate_tools": compact_candidates,
|
| 240 |
+
}
|
| 241 |
+
return (
|
| 242 |
+
"You are an agentic planner for a biomedical MCP tool graph. "
|
| 243 |
+
"Plan only from the provided GraphRAG candidate tools. "
|
| 244 |
+
"Choose a concise executable operation path. Use an agent reasoning step only when no candidate tool is suitable. "
|
| 245 |
+
"For exact sequence-analysis tasks such as GC content, ORF finding, restriction digests, PCR primers, "
|
| 246 |
+
"or Kozak scoring, prefer general sequence operation tools over broad NGS or database tools when present. "
|
| 247 |
+
"Do not solve the biology task; only plan tool use.\n\n"
|
| 248 |
+
"Return exactly one JSON object with this schema:\n"
|
| 249 |
+
"{\n"
|
| 250 |
+
' "reason": "short rationale",\n'
|
| 251 |
+
' "steps": [\n'
|
| 252 |
+
' {"operation": "operation_name", "candidate_index": 0, "server": "server", "tool": "tool", "rationale": "why"},\n'
|
| 253 |
+
' {"operation": "option_comparison", "provider": "agent", "tool": "reason_over_evidence", "rationale": "why"}\n'
|
| 254 |
+
" ]\n"
|
| 255 |
+
"}\n"
|
| 256 |
+
f"Use at most {top_k_tools} steps. candidate_index must refer to candidate_tools when using an MCP tool.\n\n"
|
| 257 |
+
"Planning input:\n"
|
| 258 |
+
+ json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
def _tool_plan_from_llm_json(
|
| 262 |
+
self,
|
| 263 |
+
parsed: dict,
|
| 264 |
+
candidates: list[dict],
|
| 265 |
+
tool_graph: ToolGraph,
|
| 266 |
+
*,
|
| 267 |
+
top_k_tools: int,
|
| 268 |
+
) -> list[dict]:
|
| 269 |
+
plan = []
|
| 270 |
+
candidate_by_index = {idx: item for idx, item in enumerate(candidates)}
|
| 271 |
+
candidate_by_pair = {(item.get("server"), item.get("tool")): item for item in candidates}
|
| 272 |
+
seen = set()
|
| 273 |
+
steps = parsed.get("steps", [])
|
| 274 |
+
if not isinstance(steps, list):
|
| 275 |
+
return []
|
| 276 |
+
|
| 277 |
+
for raw_step in steps[:top_k_tools]:
|
| 278 |
+
if not isinstance(raw_step, dict):
|
| 279 |
+
continue
|
| 280 |
+
operation = str(raw_step.get("operation") or raw_step.get("step") or "").strip()
|
| 281 |
+
if not operation:
|
| 282 |
+
continue
|
| 283 |
+
provider = str(raw_step.get("provider") or raw_step.get("server") or "").strip()
|
| 284 |
+
tool_name = str(raw_step.get("tool") or "").strip()
|
| 285 |
+
candidate = None
|
| 286 |
+
raw_index = raw_step.get("candidate_index")
|
| 287 |
+
if isinstance(raw_index, int):
|
| 288 |
+
candidate = candidate_by_index.get(raw_index)
|
| 289 |
+
if candidate is None and provider and tool_name:
|
| 290 |
+
candidate = candidate_by_pair.get((provider, tool_name))
|
| 291 |
+
|
| 292 |
+
if provider == "agent" or tool_name == "reason_over_evidence" or candidate is None:
|
| 293 |
+
key = ("agent", operation)
|
| 294 |
+
if key in seen:
|
| 295 |
+
continue
|
| 296 |
+
seen.add(key)
|
| 297 |
+
plan.append(
|
| 298 |
+
{
|
| 299 |
+
"step": len(plan) + 1,
|
| 300 |
+
"operation": operation,
|
| 301 |
+
"server": "agent",
|
| 302 |
+
"tool": "reason_over_evidence",
|
| 303 |
+
"stage": raw_step.get("stage") or "downstream",
|
| 304 |
+
"input_types": raw_step.get("input_types", ["text"]),
|
| 305 |
+
"output_types": raw_step.get("output_types", ["report_table"]),
|
| 306 |
+
"candidate_tool_count": len(candidates),
|
| 307 |
+
"binding_source": "agent",
|
| 308 |
+
"planning_rationale": raw_step.get("rationale", ""),
|
| 309 |
+
}
|
| 310 |
+
)
|
| 311 |
+
continue
|
| 312 |
+
|
| 313 |
+
node_id = candidate.get("node_id")
|
| 314 |
+
if node_id and node_id not in tool_graph.nodes:
|
| 315 |
+
continue
|
| 316 |
+
key = (candidate.get("server"), candidate.get("tool"), operation)
|
| 317 |
+
if key in seen:
|
| 318 |
+
continue
|
| 319 |
+
seen.add(key)
|
| 320 |
+
plan.append(
|
| 321 |
+
{
|
| 322 |
+
"step": len(plan) + 1,
|
| 323 |
+
"operation": operation,
|
| 324 |
+
"server": candidate.get("server"),
|
| 325 |
+
"tool": candidate.get("tool"),
|
| 326 |
+
"stage": candidate.get("stage") or raw_step.get("stage") or "analysis",
|
| 327 |
+
"input_types": candidate.get("consumes", []),
|
| 328 |
+
"output_types": candidate.get("produces", []),
|
| 329 |
+
"candidate_tool_count": len(candidates),
|
| 330 |
+
"binding_source": "llm_graph_rag",
|
| 331 |
+
"node_id": node_id,
|
| 332 |
+
"planning_score": candidate.get("score", 0.0),
|
| 333 |
+
"planning_rationale": raw_step.get("rationale", ""),
|
| 334 |
+
}
|
| 335 |
+
)
|
| 336 |
+
return plan
|
| 337 |
+
|
| 338 |
+
def _content_to_text(self, content) -> str:
|
| 339 |
+
if isinstance(content, list):
|
| 340 |
+
parts = []
|
| 341 |
+
for item in content:
|
| 342 |
+
if isinstance(item, dict) and "text" in item:
|
| 343 |
+
parts.append(str(item.get("text", "")))
|
| 344 |
+
else:
|
| 345 |
+
parts.append(str(item))
|
| 346 |
+
return "\n".join(parts)
|
| 347 |
+
return str(content)
|
| 348 |
+
|
| 349 |
+
def _parse_json_object(self, text: str) -> dict:
|
| 350 |
+
try:
|
| 351 |
+
payload = json.loads(text)
|
| 352 |
+
return payload if isinstance(payload, dict) else {}
|
| 353 |
+
except json.JSONDecodeError:
|
| 354 |
+
pass
|
| 355 |
+
match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.DOTALL)
|
| 356 |
+
if match:
|
| 357 |
+
try:
|
| 358 |
+
payload = json.loads(match.group(1))
|
| 359 |
+
return payload if isinstance(payload, dict) else {}
|
| 360 |
+
except json.JSONDecodeError:
|
| 361 |
+
pass
|
| 362 |
+
start = text.find("{")
|
| 363 |
+
end = text.rfind("}")
|
| 364 |
+
if start >= 0 and end > start:
|
| 365 |
+
try:
|
| 366 |
+
payload = json.loads(text[start : end + 1])
|
| 367 |
+
return payload if isinstance(payload, dict) else {}
|
| 368 |
+
except json.JSONDecodeError:
|
| 369 |
+
return {}
|
| 370 |
+
return {}
|
| 371 |
+
|
| 372 |
+
def _build_task_profile(self, query_context: dict, query_semantics: dict, task_spec: dict) -> dict:
|
| 373 |
+
text_parts = [
|
| 374 |
+
query_context.get("question", ""),
|
| 375 |
+
query_context.get("original_query", ""),
|
| 376 |
+
query_context.get("rewritten_query", ""),
|
| 377 |
+
query_context.get("retrieval_query", ""),
|
| 378 |
+
" ".join(query_context.get("subtasks", [])),
|
| 379 |
+
" ".join(query_context.get("keywords", [])),
|
| 380 |
+
]
|
| 381 |
+
text = " ".join(str(part) for part in text_parts if part).lower()
|
| 382 |
+
query_tokens = self._tokenize(text)
|
| 383 |
+
focus_tokens = {
|
| 384 |
+
token
|
| 385 |
+
for token in query_tokens
|
| 386 |
+
if len(token) >= 4 and token not in self.GENERIC_TOKENS and token not in {"text", "table", "report"}
|
| 387 |
+
}
|
| 388 |
+
resource_tokens = sorted(token for token in self.RESOURCE_TOKENS if token in text)
|
| 389 |
+
question = str(query_context.get("question", "") or query_context.get("original_query", ""))
|
| 390 |
+
categories = {str(item).lower() for item in query_context.get("categories", [])}
|
| 391 |
+
task_tags = self._infer_task_tags(text, categories)
|
| 392 |
+
return {
|
| 393 |
+
"text": text,
|
| 394 |
+
"query_tokens": sorted(query_tokens),
|
| 395 |
+
"focus_tokens": sorted(focus_tokens),
|
| 396 |
+
"resource_tokens": resource_tokens,
|
| 397 |
+
"categories": sorted(categories),
|
| 398 |
+
"has_sequence": "<sequence>" in question.lower() or bool(self.AMINO_ACID_SEQUENCE.search(question.upper())),
|
| 399 |
+
"has_variant": any(token in text for token in ("variant", "mutation", "amino-acid", "amino acid", "missense")),
|
| 400 |
+
"has_gene": any(token in text for token in ("gene", "genes")),
|
| 401 |
+
"needs_lookup": bool(resource_tokens) or "database_question_answering" in categories,
|
| 402 |
+
"needs_comparison": any(
|
| 403 |
+
token in text for token in ("which of the following", "compare", "comparison", "but not", "not according")
|
| 404 |
+
),
|
| 405 |
+
"is_multiple_choice": "which of the following" in text or "choices" in text,
|
| 406 |
+
"asks_clinical_significance": any(token in text for token in ("benign", "pathogenic", "significance")),
|
| 407 |
+
"task_output": (task_spec.get("target_output") or {}).get("semantic_type") or "report_table",
|
| 408 |
+
"semantic_ops": query_semantics.get("operations", []),
|
| 409 |
+
"task_tags": sorted(task_tags),
|
| 410 |
+
"is_seqqa": "seqqa" in task_tags,
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
def _build_step_specs(self, query_context: dict, task_spec: dict, ranked_paths: list[dict], profile: dict) -> list[dict]:
|
| 414 |
+
steps = []
|
| 415 |
+
task_tags = set(profile.get("task_tags", []))
|
| 416 |
+
|
| 417 |
+
if "base_counting" in task_tags:
|
| 418 |
+
steps.append(
|
| 419 |
+
self._step_spec(
|
| 420 |
+
"base_counting",
|
| 421 |
+
"Count nucleotide bases in the provided sequence using a deterministic sequence tool.",
|
| 422 |
+
stage="analysis",
|
| 423 |
+
tokens=["base", "bases", "count", "sequence", "gc", "composition", "seqfu", "bioawk"],
|
| 424 |
+
inputs=["text", "sequence_feature"],
|
| 425 |
+
outputs=["sequence_feature", "report_table"],
|
| 426 |
+
)
|
| 427 |
+
)
|
| 428 |
+
if "gc_content_calculation" in task_tags:
|
| 429 |
+
steps.append(
|
| 430 |
+
self._step_spec(
|
| 431 |
+
"gc_content_calculation",
|
| 432 |
+
"Compute GC percentage and rounding deterministically before mapping to the choices.",
|
| 433 |
+
stage="analysis",
|
| 434 |
+
tokens=["gc", "content", "percent", "percentage", "base", "count", "composition"],
|
| 435 |
+
inputs=["text", "sequence_feature"],
|
| 436 |
+
outputs=["report_table"],
|
| 437 |
+
)
|
| 438 |
+
)
|
| 439 |
+
if "reverse_complement" in task_tags:
|
| 440 |
+
steps.append(
|
| 441 |
+
self._step_spec(
|
| 442 |
+
"reverse_complement",
|
| 443 |
+
"Compute reverse-complement sequence coordinates when strand direction matters.",
|
| 444 |
+
stage="analysis",
|
| 445 |
+
tokens=["reverse", "complement", "revcomp", "rc", "sequence"],
|
| 446 |
+
inputs=["text", "sequence_feature"],
|
| 447 |
+
outputs=["sequence_feature"],
|
| 448 |
+
)
|
| 449 |
+
)
|
| 450 |
+
if "orf_detection" in task_tags:
|
| 451 |
+
steps.append(
|
| 452 |
+
self._step_spec(
|
| 453 |
+
"orf_detection",
|
| 454 |
+
"Find ORFs with explicit strand, start/stop, length, and translation-table assumptions.",
|
| 455 |
+
stage="analysis",
|
| 456 |
+
tokens=["orf", "open", "reading", "frame", "translate", "translation", "pyrodigal", "orfhunter"],
|
| 457 |
+
inputs=["text", "sequence_feature"],
|
| 458 |
+
outputs=["sequence_feature", "report_table"],
|
| 459 |
+
)
|
| 460 |
+
)
|
| 461 |
+
if "restriction_digest" in task_tags:
|
| 462 |
+
steps.append(
|
| 463 |
+
self._step_spec(
|
| 464 |
+
"restriction_digest",
|
| 465 |
+
"Resolve restriction enzyme recognition sites and compute digest fragments.",
|
| 466 |
+
stage="analysis",
|
| 467 |
+
tokens=["restriction", "digest", "enzyme", "cut", "fragment", "fragments", "site"],
|
| 468 |
+
inputs=["text", "sequence_feature"],
|
| 469 |
+
outputs=["sequence_feature", "report_table"],
|
| 470 |
+
)
|
| 471 |
+
)
|
| 472 |
+
if "primer_validation" in task_tags:
|
| 473 |
+
steps.append(
|
| 474 |
+
self._step_spec(
|
| 475 |
+
"primer_validation",
|
| 476 |
+
"Validate primer binding, orientation, Tm/GC, and expected amplicon or Gibson overlap.",
|
| 477 |
+
stage="analysis",
|
| 478 |
+
tokens=["primer", "primer3", "pcr", "gibson", "overlap", "amplicon", "tm", "decipher"],
|
| 479 |
+
inputs=["text", "sequence_feature"],
|
| 480 |
+
outputs=["sequence_feature", "report_table"],
|
| 481 |
+
)
|
| 482 |
+
)
|
| 483 |
+
if "sequence_alignment" in task_tags:
|
| 484 |
+
steps.append(
|
| 485 |
+
self._step_spec(
|
| 486 |
+
"sequence_alignment",
|
| 487 |
+
"Align primer, gene, or vector subsequences to validate exact or local matches.",
|
| 488 |
+
stage="analysis",
|
| 489 |
+
tokens=["align", "alignment", "mummer", "parasail", "match", "exact", "local"],
|
| 490 |
+
inputs=["text", "sequence_feature"],
|
| 491 |
+
outputs=["sequence_feature"],
|
| 492 |
+
)
|
| 493 |
+
)
|
| 494 |
+
if "kozak_scoring" in task_tags:
|
| 495 |
+
steps.append(
|
| 496 |
+
self._step_spec(
|
| 497 |
+
"kozak_scoring",
|
| 498 |
+
"Score Kozak context and translation-efficiency signals around candidate start codons.",
|
| 499 |
+
stage="analysis",
|
| 500 |
+
tokens=["kozak", "translation", "efficiency", "start", "codon", "rna"],
|
| 501 |
+
inputs=["text", "sequence_feature"],
|
| 502 |
+
outputs=["report_table"],
|
| 503 |
+
)
|
| 504 |
+
)
|
| 505 |
+
if "option_comparison" in task_tags:
|
| 506 |
+
steps.append(
|
| 507 |
+
self._step_spec(
|
| 508 |
+
"option_comparison",
|
| 509 |
+
"Map the computed result to the multiple-choice options and verify the final letter.",
|
| 510 |
+
stage="downstream",
|
| 511 |
+
tokens=["compare", "choice", "choices", "option", "answer", "verify"],
|
| 512 |
+
inputs=["report_table", "sequence_feature", "text"],
|
| 513 |
+
outputs=["report_table"],
|
| 514 |
+
)
|
| 515 |
+
)
|
| 516 |
+
|
| 517 |
+
if profile.get("has_sequence") and not profile.get("is_seqqa"):
|
| 518 |
+
steps.append(
|
| 519 |
+
self._step_spec(
|
| 520 |
+
"protein_identification",
|
| 521 |
+
"Ground the provided sequence to a protein or reference entity before downstream analysis.",
|
| 522 |
+
stage="input_acquisition",
|
| 523 |
+
tokens=["protein", "sequence", "reference", "identify", "uniprot", "blast", "ensembl", "homology"],
|
| 524 |
+
inputs=["text"],
|
| 525 |
+
outputs=["database_record"],
|
| 526 |
+
)
|
| 527 |
+
)
|
| 528 |
+
|
| 529 |
+
if profile.get("has_variant") and (profile.get("has_sequence") or profile.get("is_multiple_choice")):
|
| 530 |
+
steps.append(
|
| 531 |
+
self._step_spec(
|
| 532 |
+
"candidate_variant_extraction",
|
| 533 |
+
"Derive or compare the candidate variants against the grounded sequence context.",
|
| 534 |
+
stage="analysis",
|
| 535 |
+
tokens=["variant", "compare", "alignment", "mutation", "substitution", "sequence"],
|
| 536 |
+
inputs=["text", "database_record"],
|
| 537 |
+
outputs=["vcf", "annotated_variant_table"],
|
| 538 |
+
)
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
for resource in profile.get("resource_tokens", [])[:2]:
|
| 542 |
+
steps.append(
|
| 543 |
+
self._step_spec(
|
| 544 |
+
f"{resource}_lookup",
|
| 545 |
+
f"Query {resource} for task-specific evidence tied to the grounded entity or candidate variants.",
|
| 546 |
+
stage="analysis",
|
| 547 |
+
tokens=[resource, "query", "lookup", "record", "annotation", "database"],
|
| 548 |
+
inputs=["text", "database_record", "vcf", "annotated_variant_table"],
|
| 549 |
+
outputs=["database_record", "annotated_variant_table"],
|
| 550 |
+
)
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
if profile.get("needs_comparison"):
|
| 554 |
+
steps.append(
|
| 555 |
+
self._step_spec(
|
| 556 |
+
"option_comparison",
|
| 557 |
+
"Compare the competing options using the collected evidence and rank the supported candidate.",
|
| 558 |
+
stage="downstream",
|
| 559 |
+
tokens=["compare", "rank", "difference", "summary", "evidence", "supported"],
|
| 560 |
+
inputs=["database_record", "annotated_variant_table", "variant_summary"],
|
| 561 |
+
outputs=["variant_summary", "report_table"],
|
| 562 |
+
)
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
if not steps:
|
| 566 |
+
subtasks = [str(step).strip() for step in query_context.get("subtasks", []) if str(step).strip()]
|
| 567 |
+
for index, step in enumerate(subtasks[:4], start=1):
|
| 568 |
+
steps.append(
|
| 569 |
+
self._step_spec(
|
| 570 |
+
f"subtask_{index}",
|
| 571 |
+
step,
|
| 572 |
+
stage="analysis",
|
| 573 |
+
tokens=sorted(self._tokenize(step)),
|
| 574 |
+
inputs=task_spec.get("input_types", ["text"]),
|
| 575 |
+
outputs=[(task_spec.get("target_output") or {}).get("semantic_type") or "report_table"],
|
| 576 |
+
)
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
if not steps and ranked_paths:
|
| 580 |
+
best_path = ranked_paths[0]
|
| 581 |
+
for operation in best_path.get("operations", []):
|
| 582 |
+
operation_spec = self.schema_extractor.OPERATION_SPECS.get(operation, {})
|
| 583 |
+
steps.append(
|
| 584 |
+
self._step_spec(
|
| 585 |
+
operation,
|
| 586 |
+
f"Execute graph operation {operation}.",
|
| 587 |
+
stage=operation_spec.get("stage", "analysis"),
|
| 588 |
+
tokens=sorted(operation_spec.get("patterns", [])),
|
| 589 |
+
inputs=operation_spec.get("accepts", ["text"]),
|
| 590 |
+
outputs=operation_spec.get("produces", ["report_table"]),
|
| 591 |
+
)
|
| 592 |
+
)
|
| 593 |
+
|
| 594 |
+
return self._dedupe_steps(steps)
|
| 595 |
+
|
| 596 |
+
def _step_spec(
|
| 597 |
+
self,
|
| 598 |
+
label: str,
|
| 599 |
+
objective: str,
|
| 600 |
+
*,
|
| 601 |
+
stage: str,
|
| 602 |
+
tokens: list[str],
|
| 603 |
+
inputs: list[str],
|
| 604 |
+
outputs: list[str],
|
| 605 |
+
) -> dict:
|
| 606 |
+
return {
|
| 607 |
+
"label": label,
|
| 608 |
+
"objective": objective,
|
| 609 |
+
"stage": stage,
|
| 610 |
+
"tokens": sorted({token.lower() for token in tokens if token}),
|
| 611 |
+
"inputs": inputs,
|
| 612 |
+
"outputs": outputs,
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
def _dedupe_steps(self, steps: list[dict]) -> list[dict]:
|
| 616 |
+
deduped = []
|
| 617 |
+
seen = set()
|
| 618 |
+
for step in steps:
|
| 619 |
+
key = step["label"]
|
| 620 |
+
if key in seen:
|
| 621 |
+
continue
|
| 622 |
+
seen.add(key)
|
| 623 |
+
deduped.append(step)
|
| 624 |
+
return deduped
|
| 625 |
+
|
| 626 |
+
def _select_tool_chain(
|
| 627 |
+
self,
|
| 628 |
+
step_specs: list[dict],
|
| 629 |
+
profile: dict,
|
| 630 |
+
tool_graph: ToolGraph,
|
| 631 |
+
*,
|
| 632 |
+
top_k_tools: int,
|
| 633 |
+
graph_rag_candidates: list[dict] | None = None,
|
| 634 |
+
) -> list[dict]:
|
| 635 |
+
tool_plan = []
|
| 636 |
+
previous_step = None
|
| 637 |
+
used_nodes = set()
|
| 638 |
+
candidate_nodes = {item.get("node_id") for item in graph_rag_candidates or [] if item.get("node_id")}
|
| 639 |
+
candidate_scores = {item.get("node_id"): float(item.get("score", 0.0)) for item in graph_rag_candidates or []}
|
| 640 |
+
for step_index, step_spec in enumerate(step_specs, start=1):
|
| 641 |
+
candidate = self._best_tool_for_step(
|
| 642 |
+
step_spec,
|
| 643 |
+
profile,
|
| 644 |
+
tool_graph,
|
| 645 |
+
previous_step=previous_step,
|
| 646 |
+
used_nodes=used_nodes,
|
| 647 |
+
candidate_nodes=candidate_nodes,
|
| 648 |
+
candidate_scores=candidate_scores,
|
| 649 |
+
)
|
| 650 |
+
if candidate is None:
|
| 651 |
+
tool_plan.append(
|
| 652 |
+
{
|
| 653 |
+
"step": step_index,
|
| 654 |
+
"operation": step_spec["label"],
|
| 655 |
+
"server": "agent",
|
| 656 |
+
"tool": "reason_over_evidence",
|
| 657 |
+
"stage": step_spec["stage"],
|
| 658 |
+
"input_types": step_spec["inputs"],
|
| 659 |
+
"output_types": step_spec["outputs"],
|
| 660 |
+
"candidate_tool_count": 0,
|
| 661 |
+
"binding_source": "agent",
|
| 662 |
+
"planning_rationale": "no graph tool with strong semantic alignment; keep this as an agent reasoning step",
|
| 663 |
+
}
|
| 664 |
+
)
|
| 665 |
+
previous_step = tool_plan[-1]
|
| 666 |
+
continue
|
| 667 |
+
|
| 668 |
+
used_nodes.add(candidate["node_id"])
|
| 669 |
+
tool_plan.append(
|
| 670 |
+
{
|
| 671 |
+
"step": step_index,
|
| 672 |
+
"operation": step_spec["label"],
|
| 673 |
+
"server": candidate["server"],
|
| 674 |
+
"tool": candidate["name"],
|
| 675 |
+
"stage": candidate.get("stage") or step_spec["stage"],
|
| 676 |
+
"input_types": candidate.get("consumes") or step_spec["inputs"],
|
| 677 |
+
"output_types": candidate.get("produces") or step_spec["outputs"],
|
| 678 |
+
"candidate_tool_count": candidate["candidate_count"],
|
| 679 |
+
"binding_source": "graph",
|
| 680 |
+
"node_id": candidate["node_id"],
|
| 681 |
+
"planning_score": round(candidate["score"], 3),
|
| 682 |
+
"planning_rationale": candidate["reason"],
|
| 683 |
+
}
|
| 684 |
+
)
|
| 685 |
+
previous_step = tool_plan[-1]
|
| 686 |
+
|
| 687 |
+
return tool_plan[:top_k_tools]
|
| 688 |
+
|
| 689 |
+
def _best_tool_for_step(
|
| 690 |
+
self,
|
| 691 |
+
step_spec: dict,
|
| 692 |
+
profile: dict,
|
| 693 |
+
tool_graph: ToolGraph,
|
| 694 |
+
*,
|
| 695 |
+
previous_step: dict | None,
|
| 696 |
+
used_nodes: set[str],
|
| 697 |
+
candidate_nodes: set[str] | None = None,
|
| 698 |
+
candidate_scores: dict[str, float] | None = None,
|
| 699 |
+
) -> dict | None:
|
| 700 |
+
scored = []
|
| 701 |
+
query_focus = set(profile.get("focus_tokens", []))
|
| 702 |
+
query_resources = set(profile.get("resource_tokens", []))
|
| 703 |
+
for server_name, payload in tool_graph.server_index.items():
|
| 704 |
+
for tool in payload.get("tools", []):
|
| 705 |
+
if candidate_nodes and tool.get("node_id") not in candidate_nodes:
|
| 706 |
+
continue
|
| 707 |
+
tool_text = " ".join(
|
| 708 |
+
[
|
| 709 |
+
server_name,
|
| 710 |
+
str(tool.get("name", "")),
|
| 711 |
+
str(tool.get("description", "")),
|
| 712 |
+
" ".join(tool.get("keywords", [])),
|
| 713 |
+
" ".join(tool.get("capabilities", [])),
|
| 714 |
+
" ".join(tool.get("consumes", [])),
|
| 715 |
+
" ".join(tool.get("produces", [])),
|
| 716 |
+
]
|
| 717 |
+
).lower()
|
| 718 |
+
tool_tokens = self._tokenize(tool_text)
|
| 719 |
+
score, reasons = self._score_tool_candidate(
|
| 720 |
+
tool,
|
| 721 |
+
server_name,
|
| 722 |
+
step_spec,
|
| 723 |
+
query_focus,
|
| 724 |
+
query_resources,
|
| 725 |
+
profile,
|
| 726 |
+
tool_graph,
|
| 727 |
+
previous_step,
|
| 728 |
+
reused=tool.get("node_id") in used_nodes,
|
| 729 |
+
)
|
| 730 |
+
if candidate_scores and tool.get("node_id") in candidate_scores:
|
| 731 |
+
score += min(candidate_scores[tool.get("node_id", "")], 12.0) * 0.35
|
| 732 |
+
reasons.append("graph_rag_prior")
|
| 733 |
+
if score <= 0.0:
|
| 734 |
+
continue
|
| 735 |
+
scored.append(
|
| 736 |
+
{
|
| 737 |
+
"score": score,
|
| 738 |
+
"reason": "; ".join(reasons[:4]) if reasons else "semantic alignment",
|
| 739 |
+
"reason_labels": reasons,
|
| 740 |
+
"node_id": tool["node_id"],
|
| 741 |
+
"server": server_name,
|
| 742 |
+
"name": tool.get("name"),
|
| 743 |
+
"stage": tool.get("stage"),
|
| 744 |
+
"consumes": tool.get("consumes", []),
|
| 745 |
+
"produces": tool.get("produces", []),
|
| 746 |
+
"tool_tokens": sorted(tool_tokens),
|
| 747 |
+
}
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
scored.sort(key=lambda item: (item["score"], item["server"], item["name"] or ""), reverse=True)
|
| 751 |
+
if not scored:
|
| 752 |
+
if candidate_nodes:
|
| 753 |
+
return self._best_tool_for_step(
|
| 754 |
+
step_spec,
|
| 755 |
+
profile,
|
| 756 |
+
tool_graph,
|
| 757 |
+
previous_step=previous_step,
|
| 758 |
+
used_nodes=used_nodes,
|
| 759 |
+
candidate_nodes=None,
|
| 760 |
+
candidate_scores=None,
|
| 761 |
+
)
|
| 762 |
+
return None
|
| 763 |
+
best = dict(scored[0])
|
| 764 |
+
best["candidate_count"] = len(scored)
|
| 765 |
+
if best["score"] < 4.5:
|
| 766 |
+
return None
|
| 767 |
+
if not self._candidate_meets_step_contract(best, step_spec, profile):
|
| 768 |
+
return None
|
| 769 |
+
return best
|
| 770 |
+
|
| 771 |
+
def _infer_task_tags(self, text: str, categories: set[str]) -> set[str]:
|
| 772 |
+
tags = set()
|
| 773 |
+
if "seqqa" in categories or "seqqa" in text or re.search(r"\b[acgtu]{30,}\b", text, flags=re.IGNORECASE):
|
| 774 |
+
tags.add("seqqa")
|
| 775 |
+
if self._asks_gc_content(text):
|
| 776 |
+
tags.update({"base_counting", "gc_content_calculation"})
|
| 777 |
+
if any(token in text for token in ("reverse complement", "revcomp")):
|
| 778 |
+
tags.add("reverse_complement")
|
| 779 |
+
if any(token in text for token in ("orf", "open reading frame", "aa encoded", "amino acid", "translation efficiency")):
|
| 780 |
+
tags.add("orf_detection")
|
| 781 |
+
if any(token in text for token in ("restriction", "digest", "enzyme", "fragment", "fragments")):
|
| 782 |
+
tags.add("restriction_digest")
|
| 783 |
+
if any(token in text for token in ("primer", "pcr", "gibson", "linearize", "linearized", "clone")):
|
| 784 |
+
tags.update({"primer_validation", "sequence_alignment"})
|
| 785 |
+
if any(token in text for token in ("kozak", "translation efficiency", "human cell")):
|
| 786 |
+
tags.add("kozak_scoring")
|
| 787 |
+
if any(token in text for token in ("which of the following", "choices", "(a)", "(b)")):
|
| 788 |
+
tags.add("option_comparison")
|
| 789 |
+
return tags
|
| 790 |
+
|
| 791 |
+
def _asks_gc_content(self, text: str) -> bool:
|
| 792 |
+
return bool(
|
| 793 |
+
re.search(r"\b(percent|percentage)\s+gc\b", text)
|
| 794 |
+
or re.search(r"\bgc\s+(content|percent|percentage)\b", text)
|
| 795 |
+
or re.search(r"\bwhat\s+is\s+the\s+gc\b", text)
|
| 796 |
+
)
|
| 797 |
+
|
| 798 |
+
def _score_tool_candidate(
|
| 799 |
+
self,
|
| 800 |
+
tool: dict,
|
| 801 |
+
server_name: str,
|
| 802 |
+
step_spec: dict,
|
| 803 |
+
query_focus: set[str],
|
| 804 |
+
query_resources: set[str],
|
| 805 |
+
profile: dict,
|
| 806 |
+
tool_graph: ToolGraph,
|
| 807 |
+
previous_step: dict | None,
|
| 808 |
+
*,
|
| 809 |
+
reused: bool,
|
| 810 |
+
) -> tuple[float, list[str]]:
|
| 811 |
+
text = " ".join(
|
| 812 |
+
[
|
| 813 |
+
server_name,
|
| 814 |
+
str(tool.get("name", "")),
|
| 815 |
+
str(tool.get("description", "")),
|
| 816 |
+
" ".join(tool.get("keywords", [])),
|
| 817 |
+
" ".join(tool.get("capabilities", [])),
|
| 818 |
+
" ".join(tool.get("consumes", [])),
|
| 819 |
+
" ".join(tool.get("produces", [])),
|
| 820 |
+
]
|
| 821 |
+
).lower()
|
| 822 |
+
tool_tokens = self._tokenize(text)
|
| 823 |
+
step_tokens = set(step_spec.get("tokens", []))
|
| 824 |
+
|
| 825 |
+
score = 0.0
|
| 826 |
+
reasons = []
|
| 827 |
+
|
| 828 |
+
token_hits = step_tokens & tool_tokens
|
| 829 |
+
if token_hits:
|
| 830 |
+
score += min(len(token_hits), 6) * 1.4
|
| 831 |
+
reasons.append("step_tokens=" + ",".join(sorted(list(token_hits))[:4]))
|
| 832 |
+
|
| 833 |
+
focus_hits = query_focus & tool_tokens
|
| 834 |
+
if focus_hits:
|
| 835 |
+
score += min(len(focus_hits), 5) * 0.9
|
| 836 |
+
reasons.append("query_focus=" + ",".join(sorted(list(focus_hits))[:4]))
|
| 837 |
+
|
| 838 |
+
resource_hits = query_resources & tool_tokens
|
| 839 |
+
if resource_hits:
|
| 840 |
+
score += len(resource_hits) * 2.2
|
| 841 |
+
reasons.append("resource=" + ",".join(sorted(resource_hits)))
|
| 842 |
+
|
| 843 |
+
stage = tool.get("stage") or "analysis"
|
| 844 |
+
if stage == step_spec.get("stage"):
|
| 845 |
+
score += 1.2
|
| 846 |
+
reasons.append("stage_match")
|
| 847 |
+
|
| 848 |
+
desired_inputs = set(step_spec.get("inputs", []))
|
| 849 |
+
desired_outputs = set(step_spec.get("outputs", []))
|
| 850 |
+
tool_inputs = set(tool.get("consumes", []))
|
| 851 |
+
tool_outputs = set(tool.get("produces", []))
|
| 852 |
+
io_hits = (desired_inputs & tool_inputs) | (desired_outputs & tool_outputs)
|
| 853 |
+
if io_hits:
|
| 854 |
+
score += min(len(io_hits), 3) * 1.1
|
| 855 |
+
reasons.append("io_match=" + ",".join(sorted(list(io_hits))[:3]))
|
| 856 |
+
elif desired_outputs and tool_outputs and not (desired_outputs & tool_outputs):
|
| 857 |
+
score -= 1.8
|
| 858 |
+
reasons.append("io_mismatch")
|
| 859 |
+
|
| 860 |
+
step_label = step_spec.get("label", "")
|
| 861 |
+
if step_label == "protein_identification" and any(token in text for token in ("blast", "uniprot", "ensembl", "hhblits")):
|
| 862 |
+
score += 3.0
|
| 863 |
+
reasons.append("protein_grounding_tool")
|
| 864 |
+
if step_label == "candidate_variant_extraction" and any(
|
| 865 |
+
token in text for token in ("variant", "align", "blast", "consensus", "compare", "mutation")
|
| 866 |
+
):
|
| 867 |
+
score += 2.5
|
| 868 |
+
reasons.append("variant_extraction_tool")
|
| 869 |
+
if step_label.endswith("_lookup") and any(token in text for token in ("query", "lookup", "search", "fetch", "database")):
|
| 870 |
+
score += 2.5
|
| 871 |
+
reasons.append("lookup_tool")
|
| 872 |
+
if step_label == "option_comparison" and any(token in text for token in ("compare", "summary", "annotation", "effect", "query")):
|
| 873 |
+
score += 1.8
|
| 874 |
+
reasons.append("comparison_support")
|
| 875 |
+
if step_label in {"base_counting", "gc_content_calculation"}:
|
| 876 |
+
if {"bases", "count", "atgc", "alphabet", "composition"} & tool_tokens:
|
| 877 |
+
score += 4.5
|
| 878 |
+
reasons.append("base_counting_tool")
|
| 879 |
+
if {"derep", "dereplicate", "duplicate", "grep", "filter", "extract"} & tool_tokens and not {
|
| 880 |
+
"bases",
|
| 881 |
+
"atgc",
|
| 882 |
+
"alphabet",
|
| 883 |
+
} & tool_tokens:
|
| 884 |
+
score -= 5.0
|
| 885 |
+
reasons.append("base_counting_wrong_operation_penalty")
|
| 886 |
+
if {"grep", "filter", "extract"} & tool_tokens and not {"bases", "atgc"} & tool_tokens:
|
| 887 |
+
score -= 3.5
|
| 888 |
+
reasons.append("base_counting_filter_penalty")
|
| 889 |
+
if step_label == "reverse_complement" and {"reverse", "complement", "rc"} & tool_tokens:
|
| 890 |
+
score += 4.0
|
| 891 |
+
reasons.append("reverse_complement_tool")
|
| 892 |
+
if step_label == "orf_detection" and {"orf", "orfs", "pyrodigal", "orfhunter", "translate"} & tool_tokens:
|
| 893 |
+
score += 5.0
|
| 894 |
+
reasons.append("orf_tool")
|
| 895 |
+
if step_label == "restriction_digest" and {"restriction", "digest", "enzyme", "cut", "fragment"} & tool_tokens:
|
| 896 |
+
score += 4.0
|
| 897 |
+
reasons.append("restriction_digest_tool")
|
| 898 |
+
if step_label == "primer_validation" and {"primer", "primer3", "pcr", "gibson", "tm"} & tool_tokens:
|
| 899 |
+
score += 5.0
|
| 900 |
+
reasons.append("primer_tool")
|
| 901 |
+
if step_label == "sequence_alignment" and {"align", "alignment", "mummer", "parasail", "match"} & tool_tokens:
|
| 902 |
+
score += 4.5
|
| 903 |
+
reasons.append("sequence_alignment_tool")
|
| 904 |
+
if step_label == "kozak_scoring" and {"kozak", "translation", "efficiency", "rna"} & tool_tokens:
|
| 905 |
+
score += 3.5
|
| 906 |
+
reasons.append("kozak_tool")
|
| 907 |
+
if step_label.endswith("_lookup") and query_resources and not resource_hits:
|
| 908 |
+
score -= 3.0
|
| 909 |
+
reasons.append("missing_named_resource")
|
| 910 |
+
|
| 911 |
+
if previous_step:
|
| 912 |
+
connectivity = self._tool_connectivity_score(tool, previous_step, tool_graph)
|
| 913 |
+
if connectivity > 0:
|
| 914 |
+
score += connectivity
|
| 915 |
+
reasons.append("graph_connected")
|
| 916 |
+
|
| 917 |
+
if reused:
|
| 918 |
+
score -= 0.4
|
| 919 |
+
|
| 920 |
+
if "orf" in tool_tokens and "orf" not in step_tokens and "coding" not in step_tokens:
|
| 921 |
+
score -= 4.5
|
| 922 |
+
reasons.append("orf_penalty")
|
| 923 |
+
if "promoter" in tool_tokens and "promoter" not in query_focus:
|
| 924 |
+
score -= 4.5
|
| 925 |
+
reasons.append("promoter_penalty")
|
| 926 |
+
if {"csv", "export"} & tool_tokens and step_label not in {"csv_export"}:
|
| 927 |
+
score -= 3.5
|
| 928 |
+
reasons.append("export_penalty")
|
| 929 |
+
if not self._is_sequencing_workflow(profile) and tool_inputs & self.SEQUENCING_INPUT_TYPES:
|
| 930 |
+
score -= 4.0
|
| 931 |
+
reasons.append("sequencing_workflow_penalty")
|
| 932 |
+
if self._wrong_domain_penalty(tool_tokens, profile):
|
| 933 |
+
score -= 3.5
|
| 934 |
+
reasons.append("wrong_domain_penalty")
|
| 935 |
+
if "spatial" in tool_tokens and "spatial" not in query_focus:
|
| 936 |
+
score -= 3.0
|
| 937 |
+
reasons.append("domain_penalty")
|
| 938 |
+
if profile.get("asks_clinical_significance") and "consensus" in tool_tokens:
|
| 939 |
+
score -= 1.5
|
| 940 |
+
reasons.append("consensus_penalty")
|
| 941 |
+
if profile.get("asks_clinical_significance") and step_label in {
|
| 942 |
+
"candidate_variant_extraction",
|
| 943 |
+
"option_comparison",
|
| 944 |
+
} and not ({"variant", "annotation", "effect", "mutation"} & tool_tokens):
|
| 945 |
+
score -= 2.5
|
| 946 |
+
reasons.append("clinical_variant_penalty")
|
| 947 |
+
|
| 948 |
+
return score, reasons
|
| 949 |
+
|
| 950 |
+
def _tool_connectivity_score(self, tool: dict, previous_step: dict, tool_graph: ToolGraph) -> float:
|
| 951 |
+
prev_node_id = previous_step.get("node_id")
|
| 952 |
+
if not prev_node_id or prev_node_id not in tool_graph.nodes:
|
| 953 |
+
return 0.0
|
| 954 |
+
|
| 955 |
+
current_node_id = tool.get("node_id")
|
| 956 |
+
if current_node_id == prev_node_id:
|
| 957 |
+
return 0.5
|
| 958 |
+
|
| 959 |
+
for edge in tool_graph.adjacency.get(prev_node_id, []):
|
| 960 |
+
if edge.get("target") == current_node_id and edge.get("type") in self.EDGE_TYPES:
|
| 961 |
+
return 2.5
|
| 962 |
+
|
| 963 |
+
prev_outputs = set(previous_step.get("output_types", []))
|
| 964 |
+
current_inputs = set(tool.get("consumes", []))
|
| 965 |
+
if prev_outputs & current_inputs:
|
| 966 |
+
return 1.8
|
| 967 |
+
|
| 968 |
+
return 0.0
|
| 969 |
+
|
| 970 |
+
def _build_type_trace(self, tool_plan: list[dict], task_spec: dict) -> list[str]:
|
| 971 |
+
trace = []
|
| 972 |
+
default_inputs = task_spec.get("input_types", []) or ["text"]
|
| 973 |
+
if default_inputs:
|
| 974 |
+
trace.append(default_inputs[0])
|
| 975 |
+
for step in tool_plan:
|
| 976 |
+
outputs = step.get("output_types", [])
|
| 977 |
+
if outputs:
|
| 978 |
+
trace.append(outputs[0])
|
| 979 |
+
elif trace:
|
| 980 |
+
trace.append(trace[-1])
|
| 981 |
+
return trace or ["text"]
|
| 982 |
+
|
| 983 |
+
def _tokenize(self, text: str) -> set[str]:
|
| 984 |
+
normalized = text.lower().replace("-", " ").replace("/", " ").replace("_", " ")
|
| 985 |
+
return {token for token in self.TOKEN_PATTERN.findall(normalized) if token}
|
| 986 |
+
|
| 987 |
+
def _is_sequencing_workflow(self, profile: dict) -> bool:
|
| 988 |
+
text = profile.get("text", "")
|
| 989 |
+
return any(
|
| 990 |
+
token in text
|
| 991 |
+
for token in ("fastq", "rna-seq", "rnaseq", "reads", "alignment", "count matrix", "raw counts")
|
| 992 |
+
)
|
| 993 |
+
|
| 994 |
+
def _wrong_domain_penalty(self, tool_tokens: set[str], profile: dict) -> bool:
|
| 995 |
+
text = profile.get("text", "")
|
| 996 |
+
allowed = {
|
| 997 |
+
token
|
| 998 |
+
for token in self.WRONG_DOMAIN_TOKENS
|
| 999 |
+
if token in text
|
| 1000 |
+
}
|
| 1001 |
+
return bool((tool_tokens & self.WRONG_DOMAIN_TOKENS) - allowed)
|
| 1002 |
+
|
| 1003 |
+
def _candidate_meets_step_contract(self, candidate: dict, step_spec: dict, profile: dict) -> bool:
|
| 1004 |
+
step_label = step_spec.get("label", "")
|
| 1005 |
+
tool_tokens = set(candidate.get("tool_tokens", []))
|
| 1006 |
+
tool_inputs = set(candidate.get("consumes", []))
|
| 1007 |
+
tool_outputs = set(candidate.get("produces", []))
|
| 1008 |
+
reasons = set(candidate.get("reason_labels", []))
|
| 1009 |
+
|
| 1010 |
+
if step_label == "protein_identification":
|
| 1011 |
+
return bool(
|
| 1012 |
+
{"protein_grounding_tool"} & reasons
|
| 1013 |
+
or {"blast", "uniprot", "ensembl", "hhblits", "homology"} & tool_tokens
|
| 1014 |
+
or "database_record" in tool_outputs
|
| 1015 |
+
)
|
| 1016 |
+
|
| 1017 |
+
if step_label == "candidate_variant_extraction":
|
| 1018 |
+
if not tool_outputs & {"vcf", "annotated_variant_table", "variant_summary"}:
|
| 1019 |
+
return False
|
| 1020 |
+
if not {"variant", "mutation", "annotation", "effect", "align", "compare"} & tool_tokens:
|
| 1021 |
+
return False
|
| 1022 |
+
if not self._is_sequencing_workflow(profile) and tool_inputs & self.SEQUENCING_INPUT_TYPES:
|
| 1023 |
+
return False
|
| 1024 |
+
return True
|
| 1025 |
+
|
| 1026 |
+
if step_label.endswith("_lookup"):
|
| 1027 |
+
resource_name = step_label.rsplit("_", 1)[0]
|
| 1028 |
+
has_named_resource = resource_name in tool_tokens
|
| 1029 |
+
has_lookup_shape = bool({"lookup_tool"} & reasons) and bool(
|
| 1030 |
+
tool_outputs & {"database_record", "annotated_variant_table", "variant_summary"}
|
| 1031 |
+
)
|
| 1032 |
+
return has_named_resource or has_lookup_shape
|
| 1033 |
+
|
| 1034 |
+
if step_label == "option_comparison":
|
| 1035 |
+
if not tool_outputs & {"variant_summary", "report_table", "database_record", "annotated_variant_table"}:
|
| 1036 |
+
return False
|
| 1037 |
+
if not {"compare", "summary", "effect", "annotation", "rank"} & tool_tokens:
|
| 1038 |
+
return False
|
| 1039 |
+
if tool_inputs & self.SEQUENCING_INPUT_TYPES:
|
| 1040 |
+
return False
|
| 1041 |
+
return True
|
| 1042 |
+
|
| 1043 |
+
if step_label in {"base_counting", "gc_content_calculation"}:
|
| 1044 |
+
return bool({"bases", "atgc", "alphabet"} & tool_tokens or "base_counting_tool" in reasons)
|
| 1045 |
+
|
| 1046 |
+
return True
|
Biomanus_upload/biomni/graph/schema_extractor.py
ADDED
|
@@ -0,0 +1,1180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from collections import Counter
|
| 5 |
+
|
| 6 |
+
from biomni.graph.workflow_priors import WORKFLOW_PRIORS, rank_workflow_priors
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ToolSchemaExtractor:
|
| 10 |
+
"""Infer lightweight graph semantics from MCP/tool metadata."""
|
| 11 |
+
|
| 12 |
+
CAPABILITY_PATTERNS = {
|
| 13 |
+
"normalization": {"normalize", "normalization", "normalise", "normalisation", "size factor", "vst", "rlog"},
|
| 14 |
+
"quantification": {"quant", "quantification", "salmon", "kallisto", "abundance", "transcript"},
|
| 15 |
+
"read_counting": {"featurecounts", "htseq", "count reads", "read count", "summarizeoverlaps"},
|
| 16 |
+
"differential_expression": {
|
| 17 |
+
"deseq2",
|
| 18 |
+
"differential expression",
|
| 19 |
+
"differentially expressed",
|
| 20 |
+
"deg",
|
| 21 |
+
"edger",
|
| 22 |
+
"limma",
|
| 23 |
+
"log2fc",
|
| 24 |
+
"fold change",
|
| 25 |
+
},
|
| 26 |
+
"pathway_enrichment": {
|
| 27 |
+
"gsea",
|
| 28 |
+
"fgsea",
|
| 29 |
+
"clusterprofiler",
|
| 30 |
+
"enrichment",
|
| 31 |
+
"gene set",
|
| 32 |
+
"msigdb",
|
| 33 |
+
"kegg",
|
| 34 |
+
"reactome",
|
| 35 |
+
"gene ontology",
|
| 36 |
+
},
|
| 37 |
+
"data_inspection": {"inspect", "summary", "schema", "preview", "describe", "table"},
|
| 38 |
+
"exact_sequence_analysis": {"exact short-sequence primitive", "exact sequence primitive"},
|
| 39 |
+
"format_conversion": {"convert", "transform", "parse", "merge", "filter", "export"},
|
| 40 |
+
"single_cell_analysis": {"single-cell", "singlecell", "scrna", "scanpy", "seurat", "clustering", "umap"},
|
| 41 |
+
"alignment": {"align", "alignment", "bwa", "star", "map"},
|
| 42 |
+
"variant_calling": {"variant", "vcf", "gatk", "snp", "mutation"},
|
| 43 |
+
"variant_filtering": {"filter variant", "bcftools filter", "vcf filter", "normalize variant"},
|
| 44 |
+
"taxonomic_classification": {"taxonomy", "taxonomic", "kraken", "metaphlan", "centrifuge", "bracken"},
|
| 45 |
+
"genome_annotation": {"prokka", "annotate genome", "genome annotation"},
|
| 46 |
+
"orthology_clustering": {"orthofinder", "ortholog", "orthogroup", "cluster annotation"},
|
| 47 |
+
"annotation": {"annotate", "annotation", "label", "classify"},
|
| 48 |
+
"visualization": {"plot", "visual", "umap", "tsne", "heatmap", "volcano"},
|
| 49 |
+
"database_search": {
|
| 50 |
+
"database",
|
| 51 |
+
"lookup",
|
| 52 |
+
"search",
|
| 53 |
+
"pubmed",
|
| 54 |
+
"ensembl",
|
| 55 |
+
"clinvar",
|
| 56 |
+
"uniprot",
|
| 57 |
+
"biomart",
|
| 58 |
+
"msigdb",
|
| 59 |
+
"mirdb",
|
| 60 |
+
"gtrd",
|
| 61 |
+
"gene set",
|
| 62 |
+
},
|
| 63 |
+
"quality_control": {"qc", "quality", "filter", "trim", "clean"},
|
| 64 |
+
"statistics": {"stat", "qvalue", "pvalue", "regression", "test", "significance"},
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
DB_RESOURCE_HINTS = {
|
| 68 |
+
"ensembl",
|
| 69 |
+
"clinvar",
|
| 70 |
+
"uniprot",
|
| 71 |
+
"pubmed",
|
| 72 |
+
"biomart",
|
| 73 |
+
"msigdb",
|
| 74 |
+
"mirdb",
|
| 75 |
+
"gtrd",
|
| 76 |
+
"gene set",
|
| 77 |
+
"ontology",
|
| 78 |
+
"disease ontology",
|
| 79 |
+
"cell atlas",
|
| 80 |
+
"annotation database",
|
| 81 |
+
"metadata query",
|
| 82 |
+
"association table",
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
PATHWAY_HINTS = {
|
| 86 |
+
"gsea",
|
| 87 |
+
"fgsea",
|
| 88 |
+
"clusterprofiler",
|
| 89 |
+
"enrichment",
|
| 90 |
+
"gene set",
|
| 91 |
+
"pathway",
|
| 92 |
+
"kegg",
|
| 93 |
+
"reactome",
|
| 94 |
+
"gene ontology",
|
| 95 |
+
"msigdb",
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
SINGLE_CELL_HINTS = {
|
| 99 |
+
"single-cell",
|
| 100 |
+
"singlecell",
|
| 101 |
+
"scrna",
|
| 102 |
+
"scanpy",
|
| 103 |
+
"seurat",
|
| 104 |
+
"singlecellexperiment",
|
| 105 |
+
"cell atlas",
|
| 106 |
+
"cell type",
|
| 107 |
+
"umap",
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
DIFFERENTIAL_EXPRESSION_HINTS = {
|
| 111 |
+
"differential expression",
|
| 112 |
+
"deseq2",
|
| 113 |
+
"edger",
|
| 114 |
+
"limma",
|
| 115 |
+
"deg",
|
| 116 |
+
"fold change",
|
| 117 |
+
"log2fc",
|
| 118 |
+
"padj",
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
SERVER_CAPABILITY_ALLOWLISTS = {
|
| 122 |
+
"bwa": {"alignment"},
|
| 123 |
+
"hisat2": {"alignment"},
|
| 124 |
+
"star": {"alignment"},
|
| 125 |
+
"kallisto": {"quantification"},
|
| 126 |
+
"salmon": {"quantification"},
|
| 127 |
+
"bioconductor-tximport": {"quantification", "format_conversion", "read_counting"},
|
| 128 |
+
"bioconductor-deseq2": {"normalization", "differential_expression", "statistics", "visualization"},
|
| 129 |
+
"cnvkit": {"variant_calling", "variant_filtering", "annotation", "visualization"},
|
| 130 |
+
"samtools": {"alignment", "format_conversion", "data_inspection"},
|
| 131 |
+
"bcftools": {"variant_calling", "variant_filtering", "annotation", "format_conversion"},
|
| 132 |
+
"gatk": {"variant_calling", "variant_filtering"},
|
| 133 |
+
"gatk4": {"variant_calling", "variant_filtering"},
|
| 134 |
+
"freebayes": {"variant_calling"},
|
| 135 |
+
"ucsc-cell-browser": {"single_cell_analysis", "visualization", "data_inspection"},
|
| 136 |
+
"vpt": {"statistics", "annotation", "format_conversion"},
|
| 137 |
+
"r-dwls": {"statistics", "single_cell_analysis"},
|
| 138 |
+
"seacells": {"single_cell_analysis"},
|
| 139 |
+
"harpy": {"single_cell_analysis", "statistics"},
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
DATATYPE_PATTERNS = {
|
| 143 |
+
"anndata": {"anndata", "h5ad"},
|
| 144 |
+
"annotated_variant_table": {"annotated variant", "variant annotation table", "annotated vcf"},
|
| 145 |
+
"fastq": {"fastq", "fasta", "reads"},
|
| 146 |
+
"metagenomic_reads": {"metagenomic reads", "metagenomics reads", "microbiome reads"},
|
| 147 |
+
"bam": {"bam", "sam"},
|
| 148 |
+
"vcf": {"vcf", "variant"},
|
| 149 |
+
"reference_genome": {"reference genome", "genome index", "fasta", "genomic fna"},
|
| 150 |
+
"annotation_gff": {"gff", "gtf", "genomic annotation"},
|
| 151 |
+
"quant_table": {"quant.sf", "abundance table", "transcript abundance"},
|
| 152 |
+
"csv": {"csv", "tsv", "table"},
|
| 153 |
+
"json": {"json"},
|
| 154 |
+
"rds": {"rds", "seurat", "spatial object"},
|
| 155 |
+
"count_matrix": {"count", "counts", "matrix", "raw counts"},
|
| 156 |
+
"raw_count_matrix": {
|
| 157 |
+
"raw count",
|
| 158 |
+
"raw count matrix",
|
| 159 |
+
"raw count matrices",
|
| 160 |
+
"raw counts",
|
| 161 |
+
"integer counts",
|
| 162 |
+
"count matrix",
|
| 163 |
+
"count matrices",
|
| 164 |
+
},
|
| 165 |
+
"normalized_count_matrix": {"normalized count", "normalised count", "vst", "rlog", "size factor"},
|
| 166 |
+
"sample_metadata": {"sample metadata", "metadata table", "condition", "phenotype", "group labels"},
|
| 167 |
+
"de_table": {"differential expression", "deg", "log2fc", "padj", "de table"},
|
| 168 |
+
"differential_expression_table": {
|
| 169 |
+
"differential expression table",
|
| 170 |
+
"differential expression",
|
| 171 |
+
"deg",
|
| 172 |
+
"log2fc",
|
| 173 |
+
"padj",
|
| 174 |
+
"de table",
|
| 175 |
+
},
|
| 176 |
+
"significant_gene_list": {"significant genes", "up-regulated genes", "down-regulated genes", "de genes"},
|
| 177 |
+
"ranked_gene_list": {"ranked list", "ranked genes", "prerank", "gene ranking"},
|
| 178 |
+
"gene_list": {"gene list", "genes"},
|
| 179 |
+
"gene_symbol_list": {"gene symbol", "gene symbols", "symbol list"},
|
| 180 |
+
"entrez_gene_list": {"entrez", "entrez id", "entrez gene"},
|
| 181 |
+
"pathway_table": {"pathway", "enrichment", "gsea result"},
|
| 182 |
+
"kegg_enrichment_table": {"kegg enrichment", "enrichkegg", "kegg pathway"},
|
| 183 |
+
"go_enrichment_table": {"go enrichment", "gene ontology enrichment"},
|
| 184 |
+
"merged_pathway_table": {"merged pathway", "shared pathway", "comparative pathway"},
|
| 185 |
+
"taxonomic_profile": {"taxonomic profile", "relative abundance", "taxonomy table"},
|
| 186 |
+
"differential_abundance_table": {"differential abundance", "abundance difference"},
|
| 187 |
+
"genome_assembly": {"assembly", "genome assembly", "contig", "scaffold"},
|
| 188 |
+
"cluster_table": {"cluster table", "cluster annotation", "orthogroup"},
|
| 189 |
+
"marker_gene_table": {"marker gene", "cluster marker", "cell type marker"},
|
| 190 |
+
"database_record": {"database record", "knowledge source", "annotation record", "metadata entry"},
|
| 191 |
+
"sequence_feature": {"motif", "primer", "binding site", "sequence feature"},
|
| 192 |
+
"variant_summary": {"variant summary", "variant report"},
|
| 193 |
+
"report_table": {"final table", "output table", "result table"},
|
| 194 |
+
"image": {"png", "pdf", "plot", "figure", "image"},
|
| 195 |
+
"text": {"txt", "text", "markdown", "report", "summary"},
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
OPERATION_SPECS = {
|
| 199 |
+
"database_lookup": {
|
| 200 |
+
"patterns": {"database", "lookup", "search", "disgenet", "omim", "clinvar", "ensembl", "uniprot"},
|
| 201 |
+
"accepts": ["text", "gene_symbol_list"],
|
| 202 |
+
"produces": ["database_record"],
|
| 203 |
+
"capabilities": ["database_search", "annotation"],
|
| 204 |
+
"stage": "input_acquisition",
|
| 205 |
+
},
|
| 206 |
+
"sequence_feature_extraction": {
|
| 207 |
+
"patterns": {"sequence", "fasta", "motif", "primer", "binding site", "reverse complement"},
|
| 208 |
+
"accepts": ["text", "fastq", "reference_genome"],
|
| 209 |
+
"produces": ["sequence_feature"],
|
| 210 |
+
"capabilities": ["alignment", "annotation", "format_conversion"],
|
| 211 |
+
"stage": "analysis",
|
| 212 |
+
},
|
| 213 |
+
"base_counting": {
|
| 214 |
+
"patterns": {"base count", "count bases", "nucleotide count", "composition", "alphabet frequency"},
|
| 215 |
+
"accepts": ["text", "sequence_feature"],
|
| 216 |
+
"produces": ["sequence_feature", "report_table"],
|
| 217 |
+
"capabilities": ["statistics", "data_inspection", "exact_sequence_analysis"],
|
| 218 |
+
"stage": "analysis",
|
| 219 |
+
},
|
| 220 |
+
"gc_content_calculation": {
|
| 221 |
+
"patterns": {"gc content", "percent gc", "gc percentage", "base composition"},
|
| 222 |
+
"accepts": ["text", "sequence_feature"],
|
| 223 |
+
"produces": ["report_table"],
|
| 224 |
+
"capabilities": ["statistics", "data_inspection", "exact_sequence_analysis"],
|
| 225 |
+
"stage": "analysis",
|
| 226 |
+
},
|
| 227 |
+
"reverse_complement": {
|
| 228 |
+
"patterns": {"reverse complement", "revcomp", "reverse-complement", "rc sequence"},
|
| 229 |
+
"accepts": ["text", "sequence_feature"],
|
| 230 |
+
"produces": ["sequence_feature"],
|
| 231 |
+
"capabilities": ["format_conversion", "sequence_feature_extraction", "exact_sequence_analysis"],
|
| 232 |
+
"stage": "analysis",
|
| 233 |
+
},
|
| 234 |
+
"orf_detection": {
|
| 235 |
+
"patterns": {"orf", "open reading frame", "translate", "translation", "coding sequence"},
|
| 236 |
+
"accepts": ["text", "sequence_feature"],
|
| 237 |
+
"produces": ["sequence_feature", "report_table"],
|
| 238 |
+
"capabilities": ["annotation", "sequence_feature_extraction", "exact_sequence_analysis"],
|
| 239 |
+
"stage": "analysis",
|
| 240 |
+
},
|
| 241 |
+
"restriction_digest": {
|
| 242 |
+
"patterns": {"restriction digest", "digest", "restriction enzyme", "cut site", "fragments"},
|
| 243 |
+
"accepts": ["text", "sequence_feature"],
|
| 244 |
+
"produces": ["sequence_feature", "report_table"],
|
| 245 |
+
"capabilities": ["sequence_feature_extraction", "data_inspection", "exact_sequence_analysis"],
|
| 246 |
+
"stage": "analysis",
|
| 247 |
+
},
|
| 248 |
+
"primer_validation": {
|
| 249 |
+
"patterns": {"primer", "pcr", "gibson", "amplicon", "primer pair", "tm", "linearize", "linearized"},
|
| 250 |
+
"accepts": ["text", "sequence_feature"],
|
| 251 |
+
"produces": ["sequence_feature", "report_table"],
|
| 252 |
+
"capabilities": ["sequence_feature_extraction", "alignment", "exact_sequence_analysis"],
|
| 253 |
+
"stage": "analysis",
|
| 254 |
+
},
|
| 255 |
+
"sequence_alignment": {
|
| 256 |
+
"patterns": {"align sequence", "sequence alignment", "local alignment", "exact match", "pairwise alignment"},
|
| 257 |
+
"accepts": ["text", "sequence_feature"],
|
| 258 |
+
"produces": ["sequence_feature"],
|
| 259 |
+
"capabilities": ["alignment"],
|
| 260 |
+
"stage": "analysis",
|
| 261 |
+
},
|
| 262 |
+
"kozak_scoring": {
|
| 263 |
+
"patterns": {"kozak", "translation efficiency", "start codon context", "human cell"},
|
| 264 |
+
"accepts": ["text", "sequence_feature"],
|
| 265 |
+
"produces": ["report_table"],
|
| 266 |
+
"capabilities": ["sequence_feature_extraction", "statistics", "exact_sequence_analysis"],
|
| 267 |
+
"stage": "analysis",
|
| 268 |
+
},
|
| 269 |
+
"quality_control": {
|
| 270 |
+
"patterns": {"fastqc", "multiqc", "quality control", "qc"},
|
| 271 |
+
"accepts": ["fastq"],
|
| 272 |
+
"produces": ["text", "image"],
|
| 273 |
+
"capabilities": ["quality_control"],
|
| 274 |
+
"stage": "preprocessing",
|
| 275 |
+
},
|
| 276 |
+
"quantification": {
|
| 277 |
+
"patterns": {"quantify", "quantification", "salmon", "kallisto", "abundance", "quant.sf"},
|
| 278 |
+
"accepts": ["fastq", "bam", "reference_genome"],
|
| 279 |
+
"produces": ["quant_table"],
|
| 280 |
+
"capabilities": ["quantification"],
|
| 281 |
+
"stage": "analysis",
|
| 282 |
+
},
|
| 283 |
+
"alignment": {
|
| 284 |
+
"patterns": {"align reads", "alignment", "hisat2", "star", "bwa", "bowtie2", "map reads"},
|
| 285 |
+
"accepts": ["fastq", "reference_genome"],
|
| 286 |
+
"produces": ["bam"],
|
| 287 |
+
"capabilities": ["alignment"],
|
| 288 |
+
"stage": "preprocessing",
|
| 289 |
+
},
|
| 290 |
+
"read_counting": {
|
| 291 |
+
"patterns": {"featurecounts", "htseq", "count reads", "read counts", "gene count"},
|
| 292 |
+
"accepts": ["bam", "annotation_gff"],
|
| 293 |
+
"produces": ["raw_count_matrix"],
|
| 294 |
+
"capabilities": ["read_counting"],
|
| 295 |
+
"stage": "analysis",
|
| 296 |
+
},
|
| 297 |
+
"count_normalization": {
|
| 298 |
+
"patterns": {"normalize counts", "normalise counts", "normalization", "size factor", "vst", "rlog"},
|
| 299 |
+
"accepts": ["raw_count_matrix", "sample_metadata"],
|
| 300 |
+
"produces": ["normalized_count_matrix"],
|
| 301 |
+
"capabilities": ["normalization"],
|
| 302 |
+
"stage": "analysis",
|
| 303 |
+
},
|
| 304 |
+
"differential_expression": {
|
| 305 |
+
"patterns": {"differential expression", "deseq2", "deg", "edger", "limma", "log2fc", "padj"},
|
| 306 |
+
"accepts": ["raw_count_matrix", "normalized_count_matrix", "sample_metadata"],
|
| 307 |
+
"produces": ["differential_expression_table"],
|
| 308 |
+
"capabilities": ["differential_expression", "statistics"],
|
| 309 |
+
"constraints": ["requires_replicates", "bulk_rna_seq"],
|
| 310 |
+
"stage": "analysis",
|
| 311 |
+
},
|
| 312 |
+
"gene_filtering": {
|
| 313 |
+
"patterns": {"filter genes", "significant genes", "up-regulated", "down-regulated", "padj"},
|
| 314 |
+
"accepts": ["differential_expression_table"],
|
| 315 |
+
"produces": ["significant_gene_list", "gene_symbol_list"],
|
| 316 |
+
"capabilities": ["format_conversion", "statistics"],
|
| 317 |
+
"stage": "downstream",
|
| 318 |
+
},
|
| 319 |
+
"gene_id_conversion": {
|
| 320 |
+
"patterns": {"gene id", "convert gene", "entrez", "symbol", "ortholog", "id conversion"},
|
| 321 |
+
"accepts": ["gene_symbol_list", "significant_gene_list"],
|
| 322 |
+
"produces": ["entrez_gene_list"],
|
| 323 |
+
"capabilities": ["annotation", "format_conversion", "genome_annotation"],
|
| 324 |
+
"stage": "downstream",
|
| 325 |
+
},
|
| 326 |
+
"kegg_enrichment": {
|
| 327 |
+
"patterns": {"kegg", "pathway enrichment", "enrichkegg", "clusterprofiler"},
|
| 328 |
+
"accepts": ["entrez_gene_list", "ranked_gene_list", "significant_gene_list"],
|
| 329 |
+
"produces": ["kegg_enrichment_table"],
|
| 330 |
+
"capabilities": ["pathway_enrichment"],
|
| 331 |
+
"constraints": ["requires_entrez_id"],
|
| 332 |
+
"stage": "downstream",
|
| 333 |
+
},
|
| 334 |
+
"go_enrichment": {
|
| 335 |
+
"patterns": {"go enrichment", "gene ontology", "enrichgo", "ontology"},
|
| 336 |
+
"accepts": ["entrez_gene_list", "ranked_gene_list", "significant_gene_list"],
|
| 337 |
+
"produces": ["go_enrichment_table"],
|
| 338 |
+
"capabilities": ["pathway_enrichment"],
|
| 339 |
+
"stage": "downstream",
|
| 340 |
+
},
|
| 341 |
+
"pathway_merge": {
|
| 342 |
+
"patterns": {"compare shared pathways", "shared pathways", "merge pathways", "compare pathways"},
|
| 343 |
+
"accepts": ["kegg_enrichment_table", "go_enrichment_table", "pathway_table"],
|
| 344 |
+
"produces": ["merged_pathway_table"],
|
| 345 |
+
"capabilities": ["format_conversion", "data_inspection"],
|
| 346 |
+
"stage": "reporting",
|
| 347 |
+
},
|
| 348 |
+
"csv_export": {
|
| 349 |
+
"patterns": {"csv", "output table", "save table", "export"},
|
| 350 |
+
"accepts": [
|
| 351 |
+
"merged_pathway_table",
|
| 352 |
+
"kegg_enrichment_table",
|
| 353 |
+
"go_enrichment_table",
|
| 354 |
+
"pathway_table",
|
| 355 |
+
"differential_expression_table",
|
| 356 |
+
"variant_summary",
|
| 357 |
+
"taxonomic_profile",
|
| 358 |
+
],
|
| 359 |
+
"produces": ["report_table"],
|
| 360 |
+
"capabilities": ["format_conversion"],
|
| 361 |
+
"stage": "reporting",
|
| 362 |
+
},
|
| 363 |
+
"variant_annotation": {
|
| 364 |
+
"patterns": {"variant annotation", "vep", "snpeff", "annotate variant", "clinvar"},
|
| 365 |
+
"accepts": ["vcf"],
|
| 366 |
+
"produces": ["annotated_variant_table"],
|
| 367 |
+
"capabilities": ["annotation", "variant_filtering"],
|
| 368 |
+
"stage": "analysis",
|
| 369 |
+
},
|
| 370 |
+
"variant_summary": {
|
| 371 |
+
"patterns": {"variant summary", "summarize variants", "variant report"},
|
| 372 |
+
"accepts": ["annotated_variant_table", "vcf"],
|
| 373 |
+
"produces": ["variant_summary"],
|
| 374 |
+
"capabilities": ["data_inspection", "format_conversion"],
|
| 375 |
+
"stage": "reporting",
|
| 376 |
+
},
|
| 377 |
+
"single_cell_clustering": {
|
| 378 |
+
"patterns": {"single-cell", "singlecell", "scrna", "scanpy", "seurat", "clustering", "umap"},
|
| 379 |
+
"accepts": ["anndata", "rds"],
|
| 380 |
+
"produces": ["cluster_table"],
|
| 381 |
+
"capabilities": ["single_cell_analysis"],
|
| 382 |
+
"stage": "analysis",
|
| 383 |
+
},
|
| 384 |
+
"marker_gene_detection": {
|
| 385 |
+
"patterns": {"marker genes", "cluster markers", "cell type markers"},
|
| 386 |
+
"accepts": ["anndata", "cluster_table"],
|
| 387 |
+
"produces": ["marker_gene_table", "gene_symbol_list"],
|
| 388 |
+
"capabilities": ["single_cell_analysis", "statistics"],
|
| 389 |
+
"stage": "downstream",
|
| 390 |
+
},
|
| 391 |
+
"taxonomic_classification": {
|
| 392 |
+
"patterns": {"taxonomy", "taxonomic", "kraken", "metaphlan", "centrifuge", "bracken"},
|
| 393 |
+
"accepts": ["fastq", "metagenomic_reads"],
|
| 394 |
+
"produces": ["taxonomic_profile"],
|
| 395 |
+
"capabilities": ["taxonomic_classification"],
|
| 396 |
+
"stage": "analysis",
|
| 397 |
+
},
|
| 398 |
+
"differential_abundance": {
|
| 399 |
+
"patterns": {"differential abundance", "abundance difference", "compare abundance"},
|
| 400 |
+
"accepts": ["taxonomic_profile", "sample_metadata"],
|
| 401 |
+
"produces": ["differential_abundance_table"],
|
| 402 |
+
"capabilities": ["statistics"],
|
| 403 |
+
"stage": "downstream",
|
| 404 |
+
},
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
STAGE_RULES = [
|
| 408 |
+
("input_acquisition", {"database_search"}),
|
| 409 |
+
(
|
| 410 |
+
"preprocessing",
|
| 411 |
+
{"data_inspection", "format_conversion", "quality_control", "alignment", "normalization"},
|
| 412 |
+
),
|
| 413 |
+
(
|
| 414 |
+
"analysis",
|
| 415 |
+
{
|
| 416 |
+
"quantification",
|
| 417 |
+
"read_counting",
|
| 418 |
+
"differential_expression",
|
| 419 |
+
"single_cell_analysis",
|
| 420 |
+
"variant_calling",
|
| 421 |
+
"variant_filtering",
|
| 422 |
+
"taxonomic_classification",
|
| 423 |
+
"orthology_clustering",
|
| 424 |
+
"statistics",
|
| 425 |
+
},
|
| 426 |
+
),
|
| 427 |
+
("downstream", {"pathway_enrichment", "annotation", "genome_annotation"}),
|
| 428 |
+
("reporting", {"visualization"}),
|
| 429 |
+
]
|
| 430 |
+
|
| 431 |
+
STOPWORDS = {
|
| 432 |
+
"the",
|
| 433 |
+
"and",
|
| 434 |
+
"for",
|
| 435 |
+
"with",
|
| 436 |
+
"from",
|
| 437 |
+
"that",
|
| 438 |
+
"this",
|
| 439 |
+
"into",
|
| 440 |
+
"using",
|
| 441 |
+
"data",
|
| 442 |
+
"tool",
|
| 443 |
+
"file",
|
| 444 |
+
"files",
|
| 445 |
+
"analysis",
|
| 446 |
+
"result",
|
| 447 |
+
"results",
|
| 448 |
+
"input",
|
| 449 |
+
"output",
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
CURATED_SERVER_SEMANTICS = {
|
| 453 |
+
"salmon": {
|
| 454 |
+
"capabilities": ["quantification"],
|
| 455 |
+
"consumes": ["fastq", "reference_genome"],
|
| 456 |
+
"produces": ["quant_table"],
|
| 457 |
+
"operations": ["quantification"],
|
| 458 |
+
"stage": "analysis",
|
| 459 |
+
},
|
| 460 |
+
"bioconductor-tximport": {
|
| 461 |
+
"capabilities": ["quantification", "format_conversion"],
|
| 462 |
+
"consumes": ["quant_table", "annotation_gff"],
|
| 463 |
+
"produces": ["count_matrix"],
|
| 464 |
+
"operations": ["read_counting"],
|
| 465 |
+
"stage": "analysis",
|
| 466 |
+
},
|
| 467 |
+
"bioconductor-deseq2": {
|
| 468 |
+
"capabilities": ["normalization", "differential_expression", "statistics", "visualization"],
|
| 469 |
+
"consumes": ["raw_count_matrix", "sample_metadata", "count_matrix"],
|
| 470 |
+
"produces": ["differential_expression_table", "de_table", "image"],
|
| 471 |
+
"operations": ["count_normalization", "differential_expression", "gene_filtering"],
|
| 472 |
+
"stage": "analysis",
|
| 473 |
+
},
|
| 474 |
+
"hisat2": {
|
| 475 |
+
"capabilities": ["alignment"],
|
| 476 |
+
"consumes": ["fastq", "reference_genome", "annotation_gff"],
|
| 477 |
+
"produces": ["bam"],
|
| 478 |
+
"operations": ["alignment"],
|
| 479 |
+
"stage": "preprocessing",
|
| 480 |
+
},
|
| 481 |
+
"star": {
|
| 482 |
+
"capabilities": ["alignment"],
|
| 483 |
+
"consumes": ["fastq", "reference_genome", "annotation_gff"],
|
| 484 |
+
"produces": ["bam"],
|
| 485 |
+
"operations": ["alignment"],
|
| 486 |
+
"stage": "preprocessing",
|
| 487 |
+
},
|
| 488 |
+
"htseq": {
|
| 489 |
+
"capabilities": ["read_counting"],
|
| 490 |
+
"consumes": ["bam", "annotation_gff"],
|
| 491 |
+
"produces": ["raw_count_matrix", "count_matrix"],
|
| 492 |
+
"operations": ["read_counting"],
|
| 493 |
+
"stage": "analysis",
|
| 494 |
+
},
|
| 495 |
+
"subread": {
|
| 496 |
+
"capabilities": ["alignment", "read_counting"],
|
| 497 |
+
"consumes": ["fastq", "bam", "reference_genome", "annotation_gff"],
|
| 498 |
+
"produces": ["bam", "raw_count_matrix", "count_matrix"],
|
| 499 |
+
"operations": ["alignment", "read_counting"],
|
| 500 |
+
"stage": "analysis",
|
| 501 |
+
},
|
| 502 |
+
"fastqc": {
|
| 503 |
+
"capabilities": ["quality_control"],
|
| 504 |
+
"consumes": ["fastq"],
|
| 505 |
+
"produces": ["text"],
|
| 506 |
+
"operations": ["quality_control"],
|
| 507 |
+
"stage": "preprocessing",
|
| 508 |
+
},
|
| 509 |
+
"multiqc": {
|
| 510 |
+
"capabilities": ["quality_control", "visualization"],
|
| 511 |
+
"consumes": ["text"],
|
| 512 |
+
"produces": ["text", "image"],
|
| 513 |
+
"operations": ["quality_control", "csv_export"],
|
| 514 |
+
"stage": "reporting",
|
| 515 |
+
},
|
| 516 |
+
"gffutils": {
|
| 517 |
+
"capabilities": ["genome_annotation", "format_conversion", "data_inspection"],
|
| 518 |
+
"consumes": ["annotation_gff", "reference_genome"],
|
| 519 |
+
"produces": ["annotation_gff", "gene_list", "gene_symbol_list", "csv"],
|
| 520 |
+
"operations": ["gene_id_conversion", "database_lookup"],
|
| 521 |
+
"stage": "preprocessing",
|
| 522 |
+
},
|
| 523 |
+
"bcftools": {
|
| 524 |
+
"capabilities": ["variant_calling", "variant_filtering", "annotation"],
|
| 525 |
+
"consumes": ["bam", "vcf", "reference_genome"],
|
| 526 |
+
"produces": ["vcf", "annotated_variant_table", "variant_summary"],
|
| 527 |
+
"operations": ["variant_annotation", "variant_summary"],
|
| 528 |
+
"stage": "analysis",
|
| 529 |
+
},
|
| 530 |
+
"seqfu": {
|
| 531 |
+
"capabilities": ["data_inspection", "format_conversion", "statistics"],
|
| 532 |
+
"consumes": ["text", "fastq", "sequence_feature"],
|
| 533 |
+
"produces": ["text", "sequence_feature", "report_table"],
|
| 534 |
+
"operations": ["base_counting", "gc_content_calculation", "reverse_complement", "sequence_feature_extraction"],
|
| 535 |
+
"stage": "analysis",
|
| 536 |
+
},
|
| 537 |
+
"bioawk": {
|
| 538 |
+
"capabilities": ["data_inspection", "format_conversion", "statistics"],
|
| 539 |
+
"consumes": ["text", "fastq", "sequence_feature"],
|
| 540 |
+
"produces": ["text", "sequence_feature", "report_table"],
|
| 541 |
+
"operations": ["base_counting", "gc_content_calculation", "sequence_feature_extraction"],
|
| 542 |
+
"stage": "analysis",
|
| 543 |
+
},
|
| 544 |
+
"primer3": {
|
| 545 |
+
"capabilities": ["sequence_feature_extraction", "alignment", "statistics"],
|
| 546 |
+
"consumes": ["text", "sequence_feature"],
|
| 547 |
+
"produces": ["sequence_feature", "report_table"],
|
| 548 |
+
"operations": ["primer_validation", "sequence_feature_extraction"],
|
| 549 |
+
"stage": "analysis",
|
| 550 |
+
},
|
| 551 |
+
"bioconductor-decipher": {
|
| 552 |
+
"capabilities": ["alignment", "annotation", "sequence_feature_extraction"],
|
| 553 |
+
"consumes": ["text", "sequence_feature", "fastq", "reference_genome"],
|
| 554 |
+
"produces": ["sequence_feature", "report_table"],
|
| 555 |
+
"operations": ["primer_validation", "sequence_alignment", "sequence_feature_extraction"],
|
| 556 |
+
"stage": "analysis",
|
| 557 |
+
},
|
| 558 |
+
"pyrodigal": {
|
| 559 |
+
"capabilities": ["annotation", "sequence_feature_extraction"],
|
| 560 |
+
"consumes": ["text", "sequence_feature", "reference_genome"],
|
| 561 |
+
"produces": ["sequence_feature", "report_table"],
|
| 562 |
+
"operations": ["orf_detection", "sequence_feature_extraction"],
|
| 563 |
+
"stage": "analysis",
|
| 564 |
+
},
|
| 565 |
+
"mummer": {
|
| 566 |
+
"capabilities": ["alignment", "data_inspection"],
|
| 567 |
+
"consumes": ["text", "sequence_feature", "reference_genome"],
|
| 568 |
+
"produces": ["sequence_feature", "report_table"],
|
| 569 |
+
"operations": ["sequence_alignment", "primer_validation"],
|
| 570 |
+
"stage": "analysis",
|
| 571 |
+
},
|
| 572 |
+
"parasail-python": {
|
| 573 |
+
"capabilities": ["alignment", "statistics"],
|
| 574 |
+
"consumes": ["text", "sequence_feature"],
|
| 575 |
+
"produces": ["sequence_feature", "report_table"],
|
| 576 |
+
"operations": ["sequence_alignment", "primer_validation"],
|
| 577 |
+
"stage": "analysis",
|
| 578 |
+
},
|
| 579 |
+
"viennarna": {
|
| 580 |
+
"capabilities": ["sequence_feature_extraction", "statistics"],
|
| 581 |
+
"consumes": ["text", "sequence_feature"],
|
| 582 |
+
"produces": ["sequence_feature", "report_table"],
|
| 583 |
+
"operations": ["kozak_scoring", "sequence_feature_extraction"],
|
| 584 |
+
"stage": "analysis",
|
| 585 |
+
},
|
| 586 |
+
"sequence-operations": {
|
| 587 |
+
"capabilities": ["sequence_feature_extraction", "statistics", "data_inspection", "alignment"],
|
| 588 |
+
"consumes": ["text", "sequence_feature"],
|
| 589 |
+
"produces": ["sequence_feature", "report_table"],
|
| 590 |
+
"operations": ["sequence_feature_extraction"],
|
| 591 |
+
"stage": "analysis",
|
| 592 |
+
},
|
| 593 |
+
"local-bio-cache": {
|
| 594 |
+
"capabilities": ["database_search", "alignment", "data_inspection"],
|
| 595 |
+
"consumes": ["text", "sequence_feature", "reference_genome"],
|
| 596 |
+
"produces": ["database_record", "sequence_feature", "report_table"],
|
| 597 |
+
"operations": ["database_lookup", "sequence_alignment", "sequence_feature_extraction"],
|
| 598 |
+
"stage": "analysis",
|
| 599 |
+
},
|
| 600 |
+
"gatk": {
|
| 601 |
+
"capabilities": ["variant_calling", "variant_filtering"],
|
| 602 |
+
"consumes": ["bam", "reference_genome"],
|
| 603 |
+
"produces": ["vcf"],
|
| 604 |
+
"operations": ["variant_annotation"],
|
| 605 |
+
"stage": "analysis",
|
| 606 |
+
},
|
| 607 |
+
"freebayes": {
|
| 608 |
+
"capabilities": ["variant_calling"],
|
| 609 |
+
"consumes": ["bam", "reference_genome"],
|
| 610 |
+
"produces": ["vcf"],
|
| 611 |
+
"operations": ["variant_annotation"],
|
| 612 |
+
"stage": "analysis",
|
| 613 |
+
},
|
| 614 |
+
"kraken2": {
|
| 615 |
+
"capabilities": ["taxonomic_classification"],
|
| 616 |
+
"consumes": ["fastq", "metagenomic_reads"],
|
| 617 |
+
"produces": ["taxonomic_profile"],
|
| 618 |
+
"operations": ["taxonomic_classification"],
|
| 619 |
+
"stage": "analysis",
|
| 620 |
+
},
|
| 621 |
+
"metaphlan": {
|
| 622 |
+
"capabilities": ["taxonomic_classification"],
|
| 623 |
+
"consumes": ["fastq", "metagenomic_reads"],
|
| 624 |
+
"produces": ["taxonomic_profile"],
|
| 625 |
+
"operations": ["taxonomic_classification"],
|
| 626 |
+
"stage": "analysis",
|
| 627 |
+
},
|
| 628 |
+
"prokka": {
|
| 629 |
+
"capabilities": ["genome_annotation"],
|
| 630 |
+
"consumes": ["genome_assembly"],
|
| 631 |
+
"produces": ["annotation_gff", "gene_list"],
|
| 632 |
+
"operations": ["gene_id_conversion"],
|
| 633 |
+
"stage": "analysis",
|
| 634 |
+
},
|
| 635 |
+
"orthofinder": {
|
| 636 |
+
"capabilities": ["orthology_clustering"],
|
| 637 |
+
"consumes": ["gene_list"],
|
| 638 |
+
"produces": ["cluster_table"],
|
| 639 |
+
"operations": ["gene_id_conversion"],
|
| 640 |
+
"stage": "analysis",
|
| 641 |
+
},
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
CURATED_TOOL_SEMANTICS = {
|
| 645 |
+
("sequence-operations", "gc_content"): {
|
| 646 |
+
"capabilities": ["statistics", "data_inspection"],
|
| 647 |
+
"consumes": ["text", "sequence_feature"],
|
| 648 |
+
"produces": ["report_table"],
|
| 649 |
+
"operations": ["base_counting", "gc_content_calculation"],
|
| 650 |
+
"stage": "analysis",
|
| 651 |
+
"override": True,
|
| 652 |
+
},
|
| 653 |
+
("sequence-operations", "reverse_complement"): {
|
| 654 |
+
"capabilities": ["format_conversion", "sequence_feature_extraction", "exact_sequence_analysis"],
|
| 655 |
+
"consumes": ["text", "sequence_feature"],
|
| 656 |
+
"produces": ["sequence_feature"],
|
| 657 |
+
"operations": ["reverse_complement"],
|
| 658 |
+
"stage": "analysis",
|
| 659 |
+
"override": True,
|
| 660 |
+
},
|
| 661 |
+
("sequence-operations", "find_orfs"): {
|
| 662 |
+
"capabilities": ["annotation", "sequence_feature_extraction", "exact_sequence_analysis"],
|
| 663 |
+
"consumes": ["text", "sequence_feature"],
|
| 664 |
+
"produces": ["sequence_feature", "report_table"],
|
| 665 |
+
"operations": ["orf_detection"],
|
| 666 |
+
"stage": "analysis",
|
| 667 |
+
"override": True,
|
| 668 |
+
},
|
| 669 |
+
("sequence-operations", "amino_acid_at_position_in_longest_orf"): {
|
| 670 |
+
"capabilities": ["annotation", "sequence_feature_extraction", "exact_sequence_analysis"],
|
| 671 |
+
"consumes": ["text", "sequence_feature"],
|
| 672 |
+
"produces": ["sequence_feature", "report_table"],
|
| 673 |
+
"operations": ["orf_detection"],
|
| 674 |
+
"stage": "analysis",
|
| 675 |
+
"override": True,
|
| 676 |
+
},
|
| 677 |
+
("sequence-operations", "restriction_digest"): {
|
| 678 |
+
"capabilities": ["sequence_feature_extraction", "data_inspection", "exact_sequence_analysis"],
|
| 679 |
+
"consumes": ["text", "sequence_feature"],
|
| 680 |
+
"produces": ["sequence_feature", "report_table"],
|
| 681 |
+
"operations": ["restriction_digest"],
|
| 682 |
+
"stage": "analysis",
|
| 683 |
+
"override": True,
|
| 684 |
+
},
|
| 685 |
+
("sequence-operations", "pcr_primer_analysis"): {
|
| 686 |
+
"capabilities": ["sequence_feature_extraction", "alignment", "exact_sequence_analysis"],
|
| 687 |
+
"consumes": ["text", "sequence_feature"],
|
| 688 |
+
"produces": ["sequence_feature", "report_table"],
|
| 689 |
+
"operations": ["primer_validation", "sequence_alignment"],
|
| 690 |
+
"stage": "analysis",
|
| 691 |
+
"override": True,
|
| 692 |
+
},
|
| 693 |
+
("sequence-operations", "kozak_score"): {
|
| 694 |
+
"capabilities": ["sequence_feature_extraction", "statistics", "exact_sequence_analysis"],
|
| 695 |
+
"consumes": ["text", "sequence_feature"],
|
| 696 |
+
"produces": ["report_table"],
|
| 697 |
+
"operations": ["kozak_scoring"],
|
| 698 |
+
"stage": "analysis",
|
| 699 |
+
"override": True,
|
| 700 |
+
},
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
def tokenize(self, text: str) -> set[str]:
|
| 704 |
+
return {
|
| 705 |
+
token
|
| 706 |
+
for token in re.findall(r"[a-z0-9_\-\.]{2,}", (text or "").lower())
|
| 707 |
+
if token not in self.STOPWORDS
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
def extract_tool_semantics(self, tool: dict, server_name: str | None = None) -> dict:
|
| 711 |
+
name = str(tool.get("name", ""))
|
| 712 |
+
description = str(tool.get("description", ""))
|
| 713 |
+
params = tool.get("inputSchema", {}).get("properties", {}) or tool.get("parameters", {}) or {}
|
| 714 |
+
param_text = " ".join(
|
| 715 |
+
f"{param_name} {spec.get('type', '')} {spec.get('description', '')}" for param_name, spec in params.items()
|
| 716 |
+
)
|
| 717 |
+
combined = " ".join(part for part in [server_name or "", name, description, param_text] if part)
|
| 718 |
+
tokens = self.tokenize(combined)
|
| 719 |
+
|
| 720 |
+
capabilities = self._match_labels(combined, self.CAPABILITY_PATTERNS)
|
| 721 |
+
consumes = self._infer_consumed_types(combined, params)
|
| 722 |
+
produces = self._infer_produced_types(combined, params, capabilities)
|
| 723 |
+
operations = self._infer_tool_operations(combined, capabilities)
|
| 724 |
+
constraints = self._infer_constraints(combined)
|
| 725 |
+
stage = self._infer_stage(capabilities, name, description)
|
| 726 |
+
|
| 727 |
+
curated = self._curated_semantics(server_name, name)
|
| 728 |
+
if curated:
|
| 729 |
+
if curated.get("override"):
|
| 730 |
+
capabilities = list(curated.get("capabilities", capabilities))
|
| 731 |
+
consumes = list(curated.get("consumes", consumes))
|
| 732 |
+
produces = list(curated.get("produces", produces))
|
| 733 |
+
operations = list(curated.get("operations", operations))
|
| 734 |
+
constraints = list(curated.get("constraints", constraints))
|
| 735 |
+
else:
|
| 736 |
+
capabilities = self._merge_labels(capabilities, curated.get("capabilities", []))
|
| 737 |
+
consumes = self._merge_labels(consumes, curated.get("consumes", []))
|
| 738 |
+
produces = self._merge_labels(produces, curated.get("produces", []))
|
| 739 |
+
operations = self._merge_labels(operations, curated.get("operations", []))
|
| 740 |
+
constraints = self._merge_labels(constraints, curated.get("constraints", []))
|
| 741 |
+
stage = curated.get("stage") or stage
|
| 742 |
+
|
| 743 |
+
operation_semantics = self._merge_operation_semantics(operations, consumes, produces, constraints)
|
| 744 |
+
consumes = operation_semantics["consumes"]
|
| 745 |
+
produces = operation_semantics["produces"]
|
| 746 |
+
constraints = operation_semantics["constraints"]
|
| 747 |
+
|
| 748 |
+
return {
|
| 749 |
+
"name": name,
|
| 750 |
+
"description": description,
|
| 751 |
+
"keywords": sorted(tokens),
|
| 752 |
+
"capabilities": capabilities,
|
| 753 |
+
"consumes": consumes,
|
| 754 |
+
"produces": produces,
|
| 755 |
+
"operations": operations,
|
| 756 |
+
"constraints": constraints,
|
| 757 |
+
"stage": stage,
|
| 758 |
+
"text": combined,
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
def extract_server_semantics(self, server_entry: dict) -> dict:
|
| 762 |
+
tool_semantics = [
|
| 763 |
+
self.extract_tool_semantics(tool, server_name=server_entry.get("name")) for tool in server_entry.get("tools", [])
|
| 764 |
+
]
|
| 765 |
+
capability_counts = Counter()
|
| 766 |
+
datatype_counts = Counter()
|
| 767 |
+
operation_counts = Counter()
|
| 768 |
+
constraint_counts = Counter()
|
| 769 |
+
stage_counts = Counter()
|
| 770 |
+
keywords: set[str] = self.tokenize(
|
| 771 |
+
" ".join(
|
| 772 |
+
[
|
| 773 |
+
server_entry.get("name", ""),
|
| 774 |
+
server_entry.get("summary", ""),
|
| 775 |
+
" ".join(server_entry.get("keywords", [])),
|
| 776 |
+
]
|
| 777 |
+
)
|
| 778 |
+
)
|
| 779 |
+
for tool in tool_semantics:
|
| 780 |
+
capability_counts.update(tool["capabilities"])
|
| 781 |
+
datatype_counts.update(tool["consumes"])
|
| 782 |
+
datatype_counts.update(tool["produces"])
|
| 783 |
+
operation_counts.update(tool.get("operations", []))
|
| 784 |
+
constraint_counts.update(tool.get("constraints", []))
|
| 785 |
+
if tool["stage"]:
|
| 786 |
+
stage_counts.update([tool["stage"]])
|
| 787 |
+
keywords.update(tool["keywords"])
|
| 788 |
+
|
| 789 |
+
capabilities = [item for item, _ in capability_counts.most_common()]
|
| 790 |
+
capabilities = self._sanitize_server_capabilities(server_entry, capabilities, tool_semantics)
|
| 791 |
+
stage_counts = Counter(
|
| 792 |
+
tool["stage"]
|
| 793 |
+
for tool in tool_semantics
|
| 794 |
+
if tool.get("stage") and any(cap in capabilities for cap in tool.get("capabilities", []))
|
| 795 |
+
)
|
| 796 |
+
|
| 797 |
+
return {
|
| 798 |
+
"capabilities": capabilities,
|
| 799 |
+
"datatypes": [item for item, _ in datatype_counts.most_common()],
|
| 800 |
+
"operations": [item for item, _ in operation_counts.most_common()],
|
| 801 |
+
"constraints": [item for item, _ in constraint_counts.most_common()],
|
| 802 |
+
"stages": [item for item, _ in stage_counts.most_common()],
|
| 803 |
+
"keywords": sorted(keywords),
|
| 804 |
+
"tool_semantics": tool_semantics,
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
def _curated_semantics(self, server_name: str | None, tool_name: str | None = None) -> dict:
|
| 808 |
+
if not server_name:
|
| 809 |
+
return {}
|
| 810 |
+
server_key = server_name.lower()
|
| 811 |
+
tool_key = (tool_name or "").lower()
|
| 812 |
+
if tool_key:
|
| 813 |
+
curated_tool = self.CURATED_TOOL_SEMANTICS.get((server_key, tool_key), {})
|
| 814 |
+
if curated_tool:
|
| 815 |
+
return curated_tool
|
| 816 |
+
curated = self.CURATED_SERVER_SEMANTICS.get(server_key, {})
|
| 817 |
+
if not curated and server_key.startswith("bioconductor-"):
|
| 818 |
+
curated = self.CURATED_SERVER_SEMANTICS.get(server_key.replace("_", "-"), {})
|
| 819 |
+
return curated
|
| 820 |
+
|
| 821 |
+
def _merge_labels(self, primary: list[str], additions: list[str]) -> list[str]:
|
| 822 |
+
merged = []
|
| 823 |
+
for item in list(primary or []) + list(additions or []):
|
| 824 |
+
if item and item not in merged:
|
| 825 |
+
merged.append(item)
|
| 826 |
+
return merged
|
| 827 |
+
|
| 828 |
+
def _sanitize_server_capabilities(
|
| 829 |
+
self,
|
| 830 |
+
server_entry: dict,
|
| 831 |
+
capabilities: list[str],
|
| 832 |
+
tool_semantics: list[dict],
|
| 833 |
+
) -> list[str]:
|
| 834 |
+
server_name = str(server_entry.get("name", "")).lower()
|
| 835 |
+
category = str(server_entry.get("category", "")).lower()
|
| 836 |
+
combined_text = " ".join(
|
| 837 |
+
[
|
| 838 |
+
server_name,
|
| 839 |
+
str(server_entry.get("summary", "")),
|
| 840 |
+
" ".join(server_entry.get("keywords", [])),
|
| 841 |
+
" ".join(tool.get("text", "") for tool in tool_semantics),
|
| 842 |
+
]
|
| 843 |
+
).lower()
|
| 844 |
+
|
| 845 |
+
sanitized = list(capabilities)
|
| 846 |
+
|
| 847 |
+
if "database_search" in sanitized and not any(hint in combined_text for hint in self.DB_RESOURCE_HINTS):
|
| 848 |
+
sanitized = [cap for cap in sanitized if cap != "database_search"]
|
| 849 |
+
|
| 850 |
+
if "pathway_enrichment" in sanitized and not any(hint in combined_text for hint in self.PATHWAY_HINTS):
|
| 851 |
+
sanitized = [cap for cap in sanitized if cap != "pathway_enrichment"]
|
| 852 |
+
|
| 853 |
+
if "single_cell_analysis" in sanitized and not any(hint in combined_text for hint in self.SINGLE_CELL_HINTS):
|
| 854 |
+
sanitized = [cap for cap in sanitized if cap != "single_cell_analysis"]
|
| 855 |
+
|
| 856 |
+
if "differential_expression" in sanitized and not any(
|
| 857 |
+
hint in combined_text for hint in self.DIFFERENTIAL_EXPRESSION_HINTS
|
| 858 |
+
):
|
| 859 |
+
sanitized = [cap for cap in sanitized if cap != "differential_expression"]
|
| 860 |
+
|
| 861 |
+
if category in {"single_cell", "transcriptomics"} and "database_search" in sanitized:
|
| 862 |
+
if not any(hint in combined_text for hint in {"cell atlas", "metadata query", "annotation database"}):
|
| 863 |
+
sanitized = [cap for cap in sanitized if cap != "database_search"]
|
| 864 |
+
|
| 865 |
+
allowed = self.SERVER_CAPABILITY_ALLOWLISTS.get(server_name)
|
| 866 |
+
if allowed:
|
| 867 |
+
sanitized = [cap for cap in sanitized if cap in allowed]
|
| 868 |
+
|
| 869 |
+
return sanitized
|
| 870 |
+
|
| 871 |
+
def infer_query_semantics(self, query_context: dict) -> dict:
|
| 872 |
+
text_parts = [
|
| 873 |
+
query_context.get("question", ""),
|
| 874 |
+
query_context.get("original_query", ""),
|
| 875 |
+
query_context.get("rewritten_query", ""),
|
| 876 |
+
query_context.get("retrieval_query", ""),
|
| 877 |
+
" ".join(query_context.get("subtasks", [])),
|
| 878 |
+
" ".join(query_context.get("keywords", [])),
|
| 879 |
+
" ".join(query_context.get("categories", [])),
|
| 880 |
+
]
|
| 881 |
+
text = " ".join(part for part in text_parts if part)
|
| 882 |
+
capabilities = self._match_labels(text, self.CAPABILITY_PATTERNS)
|
| 883 |
+
datatypes = self._match_labels(text, self.DATATYPE_PATTERNS)
|
| 884 |
+
operations = self._infer_operations(text)
|
| 885 |
+
for operation in operations:
|
| 886 |
+
capabilities = self._merge_labels(capabilities, self.OPERATION_SPECS.get(operation, {}).get("capabilities", []))
|
| 887 |
+
stages = self._infer_query_stages(query_context, capabilities)
|
| 888 |
+
operation_tokens = {token for operation in operations for token in operation.split("_")}
|
| 889 |
+
constraints = self._infer_constraints(text)
|
| 890 |
+
task_spec = self.infer_task_spec(query_context, text, datatypes, operations, constraints)
|
| 891 |
+
operation_anchors = self._merge_labels(operations, task_spec.get("required_operations", []))
|
| 892 |
+
return {
|
| 893 |
+
"keywords": sorted(self.tokenize(text) | operation_tokens),
|
| 894 |
+
"capabilities": capabilities,
|
| 895 |
+
"datatypes": datatypes,
|
| 896 |
+
"operations": operations,
|
| 897 |
+
"operation_anchors": operation_anchors,
|
| 898 |
+
"stages": stages,
|
| 899 |
+
"organism": self._infer_organism(text),
|
| 900 |
+
"branch_labels": self._infer_branch_labels(text),
|
| 901 |
+
"workflow_type": self._infer_workflow_type(text, operations),
|
| 902 |
+
"output_schema": self._infer_output_schema(text),
|
| 903 |
+
"constraints": constraints,
|
| 904 |
+
"task_spec": task_spec,
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
def infer_task_spec(
|
| 908 |
+
self,
|
| 909 |
+
query_context: dict,
|
| 910 |
+
text: str,
|
| 911 |
+
datatypes: list[str],
|
| 912 |
+
operations: list[str],
|
| 913 |
+
constraints: list[str],
|
| 914 |
+
) -> dict:
|
| 915 |
+
categories = {str(category).lower() for category in query_context.get("categories", [])}
|
| 916 |
+
capability_set = set(self._match_labels(text, self.CAPABILITY_PATTERNS))
|
| 917 |
+
datatype_set = set(datatypes)
|
| 918 |
+
operation_set = set(operations)
|
| 919 |
+
ranked_priors = rank_workflow_priors(
|
| 920 |
+
WORKFLOW_PRIORS,
|
| 921 |
+
text=text,
|
| 922 |
+
categories=categories,
|
| 923 |
+
capabilities=capability_set,
|
| 924 |
+
datatypes=datatype_set,
|
| 925 |
+
operations=operation_set,
|
| 926 |
+
)
|
| 927 |
+
|
| 928 |
+
input_types = list(datatypes)
|
| 929 |
+
target_output = {"semantic_type": "report_table"}
|
| 930 |
+
required_operations = list(operations)
|
| 931 |
+
assay = None
|
| 932 |
+
|
| 933 |
+
# These priors are graph-structured soft constraints. They expose
|
| 934 |
+
# operation anchors and preferred type signatures, but the concrete
|
| 935 |
+
# operation path is still selected later by constrained subgraph search.
|
| 936 |
+
for prior, _score in ranked_priors:
|
| 937 |
+
input_types = self._merge_labels(input_types, list(prior.input_types))
|
| 938 |
+
required_operations = self._merge_labels(required_operations, list(prior.operation_hints))
|
| 939 |
+
assay = assay or prior.assay
|
| 940 |
+
|
| 941 |
+
if ranked_priors:
|
| 942 |
+
best_prior, best_score = ranked_priors[0]
|
| 943 |
+
target_output = {"semantic_type": best_prior.target_output}
|
| 944 |
+
for prior, score in ranked_priors[1:]:
|
| 945 |
+
if prior.target_output == "report_table" and score >= best_score - 1.0:
|
| 946 |
+
target_output = {"semantic_type": "report_table"}
|
| 947 |
+
break
|
| 948 |
+
|
| 949 |
+
organism = self._infer_organism(text)
|
| 950 |
+
constraint_payload = {
|
| 951 |
+
"labels": constraints,
|
| 952 |
+
"organism": organism,
|
| 953 |
+
"assay": assay,
|
| 954 |
+
}
|
| 955 |
+
if organism:
|
| 956 |
+
constraint_payload["labels"] = self._merge_labels(constraint_payload["labels"], [f"organism_{organism}"])
|
| 957 |
+
|
| 958 |
+
return {
|
| 959 |
+
"input_types": input_types or ["text"],
|
| 960 |
+
"target_output": target_output,
|
| 961 |
+
"constraints": constraint_payload,
|
| 962 |
+
"required_operations": required_operations,
|
| 963 |
+
"workflow_type": self._infer_workflow_type(text, required_operations),
|
| 964 |
+
"workflow_priors": [
|
| 965 |
+
{"name": prior.name, "score": score, "target_output": prior.target_output}
|
| 966 |
+
for prior, score in ranked_priors
|
| 967 |
+
],
|
| 968 |
+
}
|
| 969 |
+
|
| 970 |
+
def _match_labels(self, text: str, pattern_map: dict[str, set[str]]) -> list[str]:
|
| 971 |
+
lowered = (text or "").lower()
|
| 972 |
+
matched = []
|
| 973 |
+
for label, hints in pattern_map.items():
|
| 974 |
+
if any(hint in lowered for hint in hints):
|
| 975 |
+
matched.append(label)
|
| 976 |
+
return matched
|
| 977 |
+
|
| 978 |
+
def _infer_operations(self, text: str) -> list[str]:
|
| 979 |
+
lowered = (text or "").lower()
|
| 980 |
+
operations = []
|
| 981 |
+
for operation, spec in self.OPERATION_SPECS.items():
|
| 982 |
+
hints = spec.get("patterns", set())
|
| 983 |
+
if any(hint in lowered for hint in hints):
|
| 984 |
+
operations.append(operation)
|
| 985 |
+
return operations
|
| 986 |
+
|
| 987 |
+
def _infer_tool_operations(self, text: str, capabilities: list[str]) -> list[str]:
|
| 988 |
+
operations = set(self._infer_operations(text))
|
| 989 |
+
capability_set = set(capabilities)
|
| 990 |
+
capability_defaults = {
|
| 991 |
+
"quality_control": ["quality_control"],
|
| 992 |
+
"alignment": ["alignment"],
|
| 993 |
+
"quantification": ["quantification"],
|
| 994 |
+
"read_counting": ["read_counting"],
|
| 995 |
+
"normalization": ["count_normalization"],
|
| 996 |
+
"differential_expression": ["differential_expression"],
|
| 997 |
+
"database_search": ["database_lookup"],
|
| 998 |
+
"taxonomic_classification": ["taxonomic_classification"],
|
| 999 |
+
"single_cell_analysis": ["single_cell_clustering"],
|
| 1000 |
+
"variant_filtering": ["variant_annotation"],
|
| 1001 |
+
}
|
| 1002 |
+
for capability, defaults in capability_defaults.items():
|
| 1003 |
+
if capability in capability_set:
|
| 1004 |
+
operations.update(defaults)
|
| 1005 |
+
return sorted(operations)
|
| 1006 |
+
|
| 1007 |
+
def _infer_constraints(self, text: str) -> list[str]:
|
| 1008 |
+
lowered = (text or "").lower()
|
| 1009 |
+
constraints = []
|
| 1010 |
+
if any(token in lowered for token in ("mouse", "murine", "mus musculus")):
|
| 1011 |
+
constraints.append("organism_mouse")
|
| 1012 |
+
if any(token in lowered for token in ("human", "homo sapiens")):
|
| 1013 |
+
constraints.append("organism_human")
|
| 1014 |
+
if any(token in lowered for token in ("bulk rna", "bulk rna-seq", "deseq2", "raw counts")):
|
| 1015 |
+
constraints.append("bulk_rna_seq")
|
| 1016 |
+
if any(token in lowered for token in ("integer count", "raw count", "raw counts")):
|
| 1017 |
+
constraints.append("requires_raw_integer_counts")
|
| 1018 |
+
if "entrez" in lowered:
|
| 1019 |
+
constraints.append("requires_entrez_id")
|
| 1020 |
+
if any(token in lowered for token in ("replicate", "replicates")):
|
| 1021 |
+
constraints.append("requires_replicates")
|
| 1022 |
+
if any(token in lowered for token in ("paired-end", "paired end", "read1", "read2")):
|
| 1023 |
+
constraints.append("paired_end_reads")
|
| 1024 |
+
return constraints
|
| 1025 |
+
|
| 1026 |
+
def _merge_operation_semantics(
|
| 1027 |
+
self,
|
| 1028 |
+
operations: list[str],
|
| 1029 |
+
consumes: list[str],
|
| 1030 |
+
produces: list[str],
|
| 1031 |
+
constraints: list[str],
|
| 1032 |
+
) -> dict:
|
| 1033 |
+
merged_consumes = list(consumes)
|
| 1034 |
+
merged_produces = list(produces)
|
| 1035 |
+
merged_constraints = list(constraints)
|
| 1036 |
+
for operation in operations:
|
| 1037 |
+
spec = self.OPERATION_SPECS.get(operation, {})
|
| 1038 |
+
merged_consumes = self._merge_labels(merged_consumes, spec.get("accepts", []))
|
| 1039 |
+
merged_produces = self._merge_labels(merged_produces, spec.get("produces", []))
|
| 1040 |
+
merged_constraints = self._merge_labels(merged_constraints, spec.get("constraints", []))
|
| 1041 |
+
return {
|
| 1042 |
+
"consumes": merged_consumes,
|
| 1043 |
+
"produces": merged_produces,
|
| 1044 |
+
"constraints": merged_constraints,
|
| 1045 |
+
}
|
| 1046 |
+
|
| 1047 |
+
def _infer_organism(self, text: str) -> str | None:
|
| 1048 |
+
lowered = (text or "").lower()
|
| 1049 |
+
if any(token in lowered for token in ("mouse", "murine", "mus musculus")):
|
| 1050 |
+
return "mouse"
|
| 1051 |
+
if any(token in lowered for token in ("human", "homo sapiens")):
|
| 1052 |
+
return "human"
|
| 1053 |
+
return None
|
| 1054 |
+
|
| 1055 |
+
def _infer_branch_labels(self, text: str) -> list[str]:
|
| 1056 |
+
labels = []
|
| 1057 |
+
for pattern in (r"\b5xfad\b", r"\b3xtg[-_ ]?ad\b", r"\bps?[30o][0o]1s\b"):
|
| 1058 |
+
for match in re.findall(pattern, text or "", flags=re.IGNORECASE):
|
| 1059 |
+
normalized = match.replace(" ", "-")
|
| 1060 |
+
if normalized.lower() == "3xtg-ad":
|
| 1061 |
+
normalized = "3xTG-AD"
|
| 1062 |
+
elif normalized.lower() == "5xfad":
|
| 1063 |
+
normalized = "5xFAD"
|
| 1064 |
+
elif normalized.lower() in {"ps301s", "ps3o1s", "p301s", "p3o1s"}:
|
| 1065 |
+
normalized = "PS301S"
|
| 1066 |
+
if normalized not in labels:
|
| 1067 |
+
labels.append(normalized)
|
| 1068 |
+
return labels
|
| 1069 |
+
|
| 1070 |
+
def _infer_workflow_type(self, text: str, operations: list[str]) -> str:
|
| 1071 |
+
lowered = (text or "").lower()
|
| 1072 |
+
has_comparison = any(token in lowered for token in ("compare", "comparative", "shared", "across"))
|
| 1073 |
+
if has_comparison and len(self._infer_branch_labels(text)) >= 2:
|
| 1074 |
+
return "fork_join_comparative_analysis"
|
| 1075 |
+
if len(operations) > 1:
|
| 1076 |
+
return "linear_typed_transformation"
|
| 1077 |
+
return "single_step"
|
| 1078 |
+
|
| 1079 |
+
def _infer_output_schema(self, text: str) -> dict:
|
| 1080 |
+
lowered = (text or "").lower()
|
| 1081 |
+
if "pathway" in lowered and any(token in lowered for token in ("5xfad", "3xtg", "ps301s", "p301s")):
|
| 1082 |
+
return {
|
| 1083 |
+
"columns": ["pathway", "5xFAD_pvalue", "3xTG_AD_pvalue", "PS3O1S_pvalue"],
|
| 1084 |
+
"join_key": "pathway",
|
| 1085 |
+
}
|
| 1086 |
+
if "csv" in lowered:
|
| 1087 |
+
return {"format": "csv"}
|
| 1088 |
+
return {}
|
| 1089 |
+
|
| 1090 |
+
def _infer_consumed_types(self, text: str, params: dict) -> list[str]:
|
| 1091 |
+
lowered = (text or "").lower()
|
| 1092 |
+
matched = set(self._match_labels(lowered, self.DATATYPE_PATTERNS))
|
| 1093 |
+
for param_name, spec in params.items():
|
| 1094 |
+
blob = f"{param_name} {spec.get('description', '')}".lower()
|
| 1095 |
+
matched.update(self._match_labels(blob, self.DATATYPE_PATTERNS))
|
| 1096 |
+
if any(keyword in param_name.lower() for keyword in ("input", "file", "path", "filename")):
|
| 1097 |
+
matched.update(self._match_labels(blob, self.DATATYPE_PATTERNS))
|
| 1098 |
+
return sorted(matched)
|
| 1099 |
+
|
| 1100 |
+
def _infer_produced_types(self, text: str, params: dict, capabilities: list[str]) -> list[str]:
|
| 1101 |
+
lowered = (text or "").lower()
|
| 1102 |
+
produced = set()
|
| 1103 |
+
for label, hints in self.DATATYPE_PATTERNS.items():
|
| 1104 |
+
if any(hint in lowered for hint in hints) and any(
|
| 1105 |
+
marker in lowered for marker in ("save", "write", "export", "output", "generate", "produce")
|
| 1106 |
+
):
|
| 1107 |
+
produced.add(label)
|
| 1108 |
+
for param_name, spec in params.items():
|
| 1109 |
+
blob = f"{param_name} {spec.get('description', '')}".lower()
|
| 1110 |
+
if any(keyword in param_name.lower() for keyword in ("output", "save", "dest", "dir")):
|
| 1111 |
+
produced.update(self._match_labels(blob, self.DATATYPE_PATTERNS))
|
| 1112 |
+
if "pathway_enrichment" in capabilities:
|
| 1113 |
+
produced.add("pathway_table")
|
| 1114 |
+
if "differential_expression" in capabilities:
|
| 1115 |
+
produced.add("de_table")
|
| 1116 |
+
if "visualization" in capabilities:
|
| 1117 |
+
produced.add("image")
|
| 1118 |
+
return sorted(produced)
|
| 1119 |
+
|
| 1120 |
+
def _infer_stage(self, capabilities: list[str], name: str, description: str) -> str:
|
| 1121 |
+
lowered = f"{name} {description}".lower()
|
| 1122 |
+
if any(token in lowered for token in ("plot", "figure", "visual")):
|
| 1123 |
+
return "reporting"
|
| 1124 |
+
# Prefer later analytical stages over generic preprocessing hints.
|
| 1125 |
+
priority_rules = [
|
| 1126 |
+
("downstream", {"pathway_enrichment", "annotation", "genome_annotation"}),
|
| 1127 |
+
(
|
| 1128 |
+
"analysis",
|
| 1129 |
+
{
|
| 1130 |
+
"quantification",
|
| 1131 |
+
"read_counting",
|
| 1132 |
+
"differential_expression",
|
| 1133 |
+
"single_cell_analysis",
|
| 1134 |
+
"variant_calling",
|
| 1135 |
+
"variant_filtering",
|
| 1136 |
+
"taxonomic_classification",
|
| 1137 |
+
"orthology_clustering",
|
| 1138 |
+
"statistics",
|
| 1139 |
+
},
|
| 1140 |
+
),
|
| 1141 |
+
("preprocessing", {"data_inspection", "format_conversion", "quality_control", "alignment", "normalization"}),
|
| 1142 |
+
("input_acquisition", {"database_search"}),
|
| 1143 |
+
]
|
| 1144 |
+
for stage, stage_capabilities in priority_rules:
|
| 1145 |
+
if any(capability in stage_capabilities for capability in capabilities):
|
| 1146 |
+
return stage
|
| 1147 |
+
for stage, stage_capabilities in self.STAGE_RULES:
|
| 1148 |
+
if any(capability in stage_capabilities for capability in capabilities):
|
| 1149 |
+
return stage
|
| 1150 |
+
return "analysis"
|
| 1151 |
+
|
| 1152 |
+
def _infer_query_stages(self, query_context: dict, capabilities: list[str]) -> list[str]:
|
| 1153 |
+
text = " ".join(query_context.get("subtasks", [])) + " " + query_context.get("rewritten_query", "")
|
| 1154 |
+
lowered = text.lower()
|
| 1155 |
+
stages = []
|
| 1156 |
+
if any(token in lowered for token in ("inspect", "load", "schema", "preview", "normalize", "normalise")):
|
| 1157 |
+
stages.append("preprocessing")
|
| 1158 |
+
if any(
|
| 1159 |
+
token in lowered
|
| 1160 |
+
for token in (
|
| 1161 |
+
"analyze",
|
| 1162 |
+
"differential",
|
| 1163 |
+
"compare",
|
| 1164 |
+
"cluster",
|
| 1165 |
+
"quantify",
|
| 1166 |
+
"count",
|
| 1167 |
+
"variant",
|
| 1168 |
+
"taxonomy",
|
| 1169 |
+
)
|
| 1170 |
+
):
|
| 1171 |
+
stages.append("analysis")
|
| 1172 |
+
if any(token in lowered for token in ("gsea", "enrichment", "pathway")):
|
| 1173 |
+
stages.append("downstream")
|
| 1174 |
+
if any(token in lowered for token in ("summarize", "report", "save plot", "visualize")):
|
| 1175 |
+
stages.append("reporting")
|
| 1176 |
+
if not stages:
|
| 1177 |
+
for stage, stage_capabilities in self.STAGE_RULES:
|
| 1178 |
+
if any(capability in stage_capabilities for capability in capabilities):
|
| 1179 |
+
stages.append(stage)
|
| 1180 |
+
return stages
|
Biomanus_upload/biomni/graph/tool_graph.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections import defaultdict
|
| 4 |
+
|
| 5 |
+
from biomni.graph.schema_extractor import ToolSchemaExtractor
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class ToolGraph:
|
| 9 |
+
"""A lightweight heterogeneous graph over MCP servers, tools, capabilities, and data types."""
|
| 10 |
+
|
| 11 |
+
def __init__(self, schema_extractor: ToolSchemaExtractor | None = None):
|
| 12 |
+
self.schema_extractor = schema_extractor or ToolSchemaExtractor()
|
| 13 |
+
self.clear()
|
| 14 |
+
|
| 15 |
+
def clear(self) -> None:
|
| 16 |
+
self.nodes: dict[str, dict] = {}
|
| 17 |
+
self.edges: list[dict] = []
|
| 18 |
+
self.adjacency: dict[str, list[dict]] = defaultdict(list)
|
| 19 |
+
self.server_entries: dict[str, dict] = {}
|
| 20 |
+
self.server_index: dict[str, dict] = {}
|
| 21 |
+
self.tool_index: dict[str, dict] = {}
|
| 22 |
+
self.operation_index: dict[str, dict] = {}
|
| 23 |
+
self.datatype_to_operations: dict[str, list[str]] = defaultdict(list)
|
| 24 |
+
|
| 25 |
+
def add_node(self, node_id: str, node_type: str, label: str, **attributes) -> None:
|
| 26 |
+
payload = {"id": node_id, "type": node_type, "label": label}
|
| 27 |
+
payload.update(attributes)
|
| 28 |
+
self.nodes[node_id] = payload
|
| 29 |
+
|
| 30 |
+
def add_edge(self, source: str, target: str, edge_type: str, weight: float = 1.0, **attributes) -> None:
|
| 31 |
+
payload = {"source": source, "target": target, "type": edge_type, "weight": weight}
|
| 32 |
+
payload.update(attributes)
|
| 33 |
+
self.edges.append(payload)
|
| 34 |
+
self.adjacency[source].append(payload)
|
| 35 |
+
|
| 36 |
+
def build_from_server_entries(self, server_entries: list[dict]) -> None:
|
| 37 |
+
self.clear()
|
| 38 |
+
for entry in server_entries:
|
| 39 |
+
self._add_server(entry)
|
| 40 |
+
self._add_cross_server_workflow_edges()
|
| 41 |
+
|
| 42 |
+
def get_server_entry(self, server_name: str) -> dict | None:
|
| 43 |
+
return self.server_entries.get(server_name)
|
| 44 |
+
|
| 45 |
+
def get_server_names(self) -> list[str]:
|
| 46 |
+
return list(self.server_entries.keys())
|
| 47 |
+
|
| 48 |
+
def get_tool_nodes_for_server(self, server_name: str) -> list[dict]:
|
| 49 |
+
return self.server_index.get(server_name, {}).get("tools", [])
|
| 50 |
+
|
| 51 |
+
def get_server_semantics(self, server_name: str) -> dict:
|
| 52 |
+
return self.server_index.get(server_name, {}).get("semantics", {})
|
| 53 |
+
|
| 54 |
+
def get_server_neighbors(self, server_name: str, max_hops: int = 2) -> list[dict]:
|
| 55 |
+
start_node = f"server:{server_name}"
|
| 56 |
+
if start_node not in self.nodes:
|
| 57 |
+
return []
|
| 58 |
+
visited = {start_node}
|
| 59 |
+
frontier = {start_node}
|
| 60 |
+
collected = []
|
| 61 |
+
for _ in range(max_hops):
|
| 62 |
+
next_frontier = set()
|
| 63 |
+
for node_id in frontier:
|
| 64 |
+
for edge in self.adjacency.get(node_id, []):
|
| 65 |
+
if edge["target"] in visited:
|
| 66 |
+
continue
|
| 67 |
+
visited.add(edge["target"])
|
| 68 |
+
next_frontier.add(edge["target"])
|
| 69 |
+
collected.append(self.nodes[edge["target"]])
|
| 70 |
+
frontier = next_frontier
|
| 71 |
+
if not frontier:
|
| 72 |
+
break
|
| 73 |
+
return collected
|
| 74 |
+
|
| 75 |
+
def _add_server(self, entry: dict) -> None:
|
| 76 |
+
server_name = entry["name"]
|
| 77 |
+
server_node = f"server:{server_name}"
|
| 78 |
+
semantics = self.schema_extractor.extract_server_semantics(entry)
|
| 79 |
+
|
| 80 |
+
self.add_node(
|
| 81 |
+
server_node,
|
| 82 |
+
"server",
|
| 83 |
+
server_name,
|
| 84 |
+
category=entry.get("category", "general"),
|
| 85 |
+
summary=entry.get("summary", ""),
|
| 86 |
+
keywords=semantics.get("keywords", []),
|
| 87 |
+
)
|
| 88 |
+
self.server_entries[server_name] = entry
|
| 89 |
+
self.server_index[server_name] = {"entry": entry, "semantics": semantics, "tools": []}
|
| 90 |
+
|
| 91 |
+
category = entry.get("category")
|
| 92 |
+
if category:
|
| 93 |
+
category_node = f"category:{category}"
|
| 94 |
+
self.add_node(category_node, "category", category)
|
| 95 |
+
self.add_edge(server_node, category_node, "categorized_as", weight=1.0)
|
| 96 |
+
|
| 97 |
+
for tool, tool_semantics in zip(entry.get("tools", []), semantics.get("tool_semantics", []), strict=False):
|
| 98 |
+
tool_name = tool_semantics["name"]
|
| 99 |
+
tool_node = f"tool:{server_name}:{tool_name}"
|
| 100 |
+
self.add_node(
|
| 101 |
+
tool_node,
|
| 102 |
+
"tool",
|
| 103 |
+
tool_name,
|
| 104 |
+
description=tool_semantics.get("description", ""),
|
| 105 |
+
keywords=tool_semantics.get("keywords", []),
|
| 106 |
+
module=entry.get("module"),
|
| 107 |
+
)
|
| 108 |
+
self.add_edge(server_node, tool_node, "hosts", weight=3.0)
|
| 109 |
+
self.server_index[server_name]["tools"].append({"node_id": tool_node, **tool, **tool_semantics})
|
| 110 |
+
self.tool_index[tool_node] = {"server_name": server_name, "tool": tool, "semantics": tool_semantics}
|
| 111 |
+
|
| 112 |
+
stage = tool_semantics.get("stage")
|
| 113 |
+
if stage:
|
| 114 |
+
stage_node = f"stage:{stage}"
|
| 115 |
+
self.add_node(stage_node, "stage", stage)
|
| 116 |
+
self.add_edge(tool_node, stage_node, "belongs_to_stage", weight=1.0)
|
| 117 |
+
|
| 118 |
+
for capability in tool_semantics.get("capabilities", []):
|
| 119 |
+
capability_node = f"capability:{capability}"
|
| 120 |
+
self.add_node(capability_node, "capability", capability)
|
| 121 |
+
self.add_edge(tool_node, capability_node, "implements", weight=2.0)
|
| 122 |
+
|
| 123 |
+
for operation in tool_semantics.get("operations", []):
|
| 124 |
+
self._add_operation_binding(operation, tool_node, tool_semantics)
|
| 125 |
+
|
| 126 |
+
for data_type in tool_semantics.get("consumes", []):
|
| 127 |
+
datatype_node = f"datatype:{data_type}"
|
| 128 |
+
self.add_node(datatype_node, "datatype", data_type)
|
| 129 |
+
self.add_edge(tool_node, datatype_node, "consumes", weight=2.0)
|
| 130 |
+
|
| 131 |
+
for data_type in tool_semantics.get("produces", []):
|
| 132 |
+
datatype_node = f"datatype:{data_type}"
|
| 133 |
+
self.add_node(datatype_node, "datatype", data_type)
|
| 134 |
+
self.add_edge(tool_node, datatype_node, "produces", weight=2.0)
|
| 135 |
+
|
| 136 |
+
for constraint in tool_semantics.get("constraints", []):
|
| 137 |
+
constraint_node = f"constraint:{constraint}"
|
| 138 |
+
self.add_node(constraint_node, "constraint", constraint)
|
| 139 |
+
self.add_edge(tool_node, constraint_node, "supports", weight=1.5)
|
| 140 |
+
|
| 141 |
+
self._add_workflow_edges(server_name)
|
| 142 |
+
|
| 143 |
+
def _add_operation_binding(self, operation: str, tool_node: str, tool_semantics: dict) -> None:
|
| 144 |
+
operation_node = f"operation:{operation}"
|
| 145 |
+
spec = self.schema_extractor.OPERATION_SPECS.get(operation, {})
|
| 146 |
+
accepts = spec.get("accepts", []) or tool_semantics.get("consumes", [])
|
| 147 |
+
produces = spec.get("produces", []) or tool_semantics.get("produces", [])
|
| 148 |
+
constraints = spec.get("constraints", []) or tool_semantics.get("constraints", [])
|
| 149 |
+
|
| 150 |
+
self.add_node(
|
| 151 |
+
operation_node,
|
| 152 |
+
"operation",
|
| 153 |
+
operation,
|
| 154 |
+
stage=spec.get("stage") or tool_semantics.get("stage"),
|
| 155 |
+
accepts=accepts,
|
| 156 |
+
produces=produces,
|
| 157 |
+
constraints=constraints,
|
| 158 |
+
)
|
| 159 |
+
self.add_edge(tool_node, operation_node, "implements_operation", weight=3.0)
|
| 160 |
+
|
| 161 |
+
entry = self.operation_index.setdefault(
|
| 162 |
+
operation,
|
| 163 |
+
{
|
| 164 |
+
"operation": operation,
|
| 165 |
+
"node_id": operation_node,
|
| 166 |
+
"accepts": [],
|
| 167 |
+
"produces": [],
|
| 168 |
+
"constraints": [],
|
| 169 |
+
"tools": [],
|
| 170 |
+
"stage": spec.get("stage") or tool_semantics.get("stage") or "analysis",
|
| 171 |
+
},
|
| 172 |
+
)
|
| 173 |
+
entry["accepts"] = self._merge_labels(entry["accepts"], accepts)
|
| 174 |
+
entry["produces"] = self._merge_labels(entry["produces"], produces)
|
| 175 |
+
entry["constraints"] = self._merge_labels(entry["constraints"], constraints)
|
| 176 |
+
entry["tools"].append({**tool_semantics, "node_id": tool_node})
|
| 177 |
+
|
| 178 |
+
for data_type in accepts:
|
| 179 |
+
datatype_node = f"datatype:{data_type}"
|
| 180 |
+
self.add_node(datatype_node, "datatype", data_type)
|
| 181 |
+
self.add_edge(operation_node, datatype_node, "accepts", weight=2.0)
|
| 182 |
+
if operation not in self.datatype_to_operations[data_type]:
|
| 183 |
+
self.datatype_to_operations[data_type].append(operation)
|
| 184 |
+
|
| 185 |
+
for data_type in produces:
|
| 186 |
+
datatype_node = f"datatype:{data_type}"
|
| 187 |
+
self.add_node(datatype_node, "datatype", data_type)
|
| 188 |
+
self.add_edge(operation_node, datatype_node, "produces", weight=2.0)
|
| 189 |
+
|
| 190 |
+
for constraint in constraints:
|
| 191 |
+
constraint_node = f"constraint:{constraint}"
|
| 192 |
+
self.add_node(constraint_node, "constraint", constraint)
|
| 193 |
+
self.add_edge(operation_node, constraint_node, "requires", weight=1.5)
|
| 194 |
+
|
| 195 |
+
def _merge_labels(self, primary: list[str], additions: list[str]) -> list[str]:
|
| 196 |
+
merged = list(primary or [])
|
| 197 |
+
for item in additions or []:
|
| 198 |
+
if item and item not in merged:
|
| 199 |
+
merged.append(item)
|
| 200 |
+
return merged
|
| 201 |
+
|
| 202 |
+
def _add_workflow_edges(self, server_name: str) -> None:
|
| 203 |
+
tools = self.server_index.get(server_name, {}).get("tools", [])
|
| 204 |
+
for source in tools:
|
| 205 |
+
source_produced = set(source.get("produces", []))
|
| 206 |
+
source_stage = source.get("stage")
|
| 207 |
+
for target in tools:
|
| 208 |
+
if source["name"] == target["name"]:
|
| 209 |
+
continue
|
| 210 |
+
target_consumed = set(target.get("consumes", []))
|
| 211 |
+
if source_produced and target_consumed and source_produced & target_consumed:
|
| 212 |
+
self.add_edge(
|
| 213 |
+
source["node_id"],
|
| 214 |
+
target["node_id"],
|
| 215 |
+
"follows",
|
| 216 |
+
weight=1.5,
|
| 217 |
+
shared_datatypes=sorted(source_produced & target_consumed),
|
| 218 |
+
)
|
| 219 |
+
elif source_stage and target.get("stage") and self._stage_distance(source_stage, target["stage"]) == 1:
|
| 220 |
+
self.add_edge(
|
| 221 |
+
source["node_id"],
|
| 222 |
+
target["node_id"],
|
| 223 |
+
"adjacent_stage",
|
| 224 |
+
weight=0.5,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
def _add_cross_server_workflow_edges(self) -> None:
|
| 228 |
+
consume_index: dict[str, list[dict]] = defaultdict(list)
|
| 229 |
+
all_tools = []
|
| 230 |
+
for server_name in self.server_index:
|
| 231 |
+
for tool in self.server_index[server_name].get("tools", []):
|
| 232 |
+
all_tools.append(tool)
|
| 233 |
+
for data_type in tool.get("consumes", []):
|
| 234 |
+
consume_index[data_type].append(tool)
|
| 235 |
+
|
| 236 |
+
generic_types = {"text", "image", "json", "csv"}
|
| 237 |
+
for source in all_tools:
|
| 238 |
+
produced_types = set(source.get("produces", [])) - generic_types
|
| 239 |
+
if not produced_types:
|
| 240 |
+
continue
|
| 241 |
+
|
| 242 |
+
candidates = []
|
| 243 |
+
for data_type in produced_types:
|
| 244 |
+
for target in consume_index.get(data_type, []):
|
| 245 |
+
if source["node_id"] == target["node_id"]:
|
| 246 |
+
continue
|
| 247 |
+
if source["node_id"].split(":")[1] == target["node_id"].split(":")[1]:
|
| 248 |
+
continue
|
| 249 |
+
stage_delta = self._stage_distance(source.get("stage"), target.get("stage"))
|
| 250 |
+
if stage_delta < 0 or stage_delta > 2:
|
| 251 |
+
continue
|
| 252 |
+
candidates.append((stage_delta, data_type, target))
|
| 253 |
+
|
| 254 |
+
candidates.sort(key=lambda item: (item[0], item[2].get("name", "")))
|
| 255 |
+
seen_targets = set()
|
| 256 |
+
for stage_delta, data_type, target in candidates[:12]:
|
| 257 |
+
if target["node_id"] in seen_targets:
|
| 258 |
+
continue
|
| 259 |
+
seen_targets.add(target["node_id"])
|
| 260 |
+
self.add_edge(
|
| 261 |
+
source["node_id"],
|
| 262 |
+
target["node_id"],
|
| 263 |
+
"typed_flow",
|
| 264 |
+
weight=2.5 if stage_delta <= 1 else 1.5,
|
| 265 |
+
shared_datatypes=[data_type],
|
| 266 |
+
cross_server=True,
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
def _stage_distance(self, source_stage: str, target_stage: str) -> int:
|
| 270 |
+
order = ["input_acquisition", "preprocessing", "analysis", "downstream", "reporting"]
|
| 271 |
+
if source_stage not in order or target_stage not in order:
|
| 272 |
+
return 99
|
| 273 |
+
return order.index(target_stage) - order.index(source_stage)
|
Biomanus_upload/biomni/graph/workflow_priors.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Iterable
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@dataclass(frozen=True)
|
| 8 |
+
class WorkflowPrior:
|
| 9 |
+
"""A graph-structured workflow prior, not a task-specific pipeline.
|
| 10 |
+
|
| 11 |
+
Priors describe soft evidence that a query belongs near a region of the
|
| 12 |
+
heterogeneous tool graph. They provide semantic anchors and preferred
|
| 13 |
+
datatypes, while the final operation path is still selected dynamically by
|
| 14 |
+
graph search over typed-flow and stage-adjacency structure.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
name: str
|
| 18 |
+
categories: frozenset[str] = frozenset()
|
| 19 |
+
capabilities: frozenset[str] = frozenset()
|
| 20 |
+
datatypes: frozenset[str] = frozenset()
|
| 21 |
+
keyword_hints: frozenset[str] = frozenset()
|
| 22 |
+
operation_hints: tuple[str, ...] = ()
|
| 23 |
+
input_types: tuple[str, ...] = ()
|
| 24 |
+
target_output: str = "report_table"
|
| 25 |
+
assay: str | None = None
|
| 26 |
+
min_score: float = 4.0
|
| 27 |
+
weight: float = 1.0
|
| 28 |
+
|
| 29 |
+
def score(
|
| 30 |
+
self,
|
| 31 |
+
*,
|
| 32 |
+
text: str,
|
| 33 |
+
categories: set[str],
|
| 34 |
+
capabilities: set[str],
|
| 35 |
+
datatypes: set[str],
|
| 36 |
+
operations: set[str],
|
| 37 |
+
) -> float:
|
| 38 |
+
lowered = (text or "").lower()
|
| 39 |
+
score = 0.0
|
| 40 |
+
score += 4.0 * len(self.categories & categories)
|
| 41 |
+
score += 2.0 * len(self.capabilities & capabilities)
|
| 42 |
+
score += 1.5 * len(self.datatypes & datatypes)
|
| 43 |
+
score += 2.0 * len(set(self.operation_hints) & operations)
|
| 44 |
+
score += sum(1.0 for hint in self.keyword_hints if hint in lowered)
|
| 45 |
+
return score * self.weight
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
WORKFLOW_PRIORS: tuple[WorkflowPrior, ...] = (
|
| 49 |
+
WorkflowPrior(
|
| 50 |
+
name="database_grounding",
|
| 51 |
+
categories=frozenset({"database_question_answering"}),
|
| 52 |
+
capabilities=frozenset({"database_search", "annotation"}),
|
| 53 |
+
keyword_hints=frozenset({"database", "clinvar", "ensembl", "uniprot", "disgenet", "omim", "mirdb", "gtrd"}),
|
| 54 |
+
operation_hints=("database_lookup",),
|
| 55 |
+
input_types=("text",),
|
| 56 |
+
target_output="database_record",
|
| 57 |
+
),
|
| 58 |
+
WorkflowPrior(
|
| 59 |
+
name="sequence_feature_reasoning",
|
| 60 |
+
categories=frozenset({"sequence_question_answering"}),
|
| 61 |
+
capabilities=frozenset({"alignment", "annotation", "format_conversion"}),
|
| 62 |
+
datatypes=frozenset({"fastq", "reference_genome", "sequence_feature"}),
|
| 63 |
+
keyword_hints=frozenset({"sequence", "fasta", "motif", "primer", "binding site", "reverse complement"}),
|
| 64 |
+
operation_hints=("sequence_feature_extraction",),
|
| 65 |
+
input_types=("text",),
|
| 66 |
+
target_output="sequence_feature",
|
| 67 |
+
),
|
| 68 |
+
WorkflowPrior(
|
| 69 |
+
name="bulk_expression_analysis",
|
| 70 |
+
capabilities=frozenset({"differential_expression", "normalization", "statistics", "read_counting"}),
|
| 71 |
+
datatypes=frozenset({"raw_count_matrix", "normalized_count_matrix", "sample_metadata", "differential_expression_table"}),
|
| 72 |
+
keyword_hints=frozenset({"rna-seq", "bulk", "count matrix", "raw counts", "deseq2", "log2fc", "padj"}),
|
| 73 |
+
operation_hints=("differential_expression",),
|
| 74 |
+
input_types=("raw_count_matrix", "sample_metadata"),
|
| 75 |
+
target_output="differential_expression_table",
|
| 76 |
+
assay="bulk_rna_seq",
|
| 77 |
+
),
|
| 78 |
+
WorkflowPrior(
|
| 79 |
+
name="pathway_interpretation",
|
| 80 |
+
capabilities=frozenset({"pathway_enrichment", "database_search", "format_conversion"}),
|
| 81 |
+
datatypes=frozenset({"differential_expression_table", "gene_symbol_list", "entrez_gene_list", "kegg_enrichment_table"}),
|
| 82 |
+
keyword_hints=frozenset({"pathway", "kegg", "enrichment", "gsea", "reactome", "gene ontology"}),
|
| 83 |
+
operation_hints=("gene_filtering", "gene_id_conversion", "kegg_enrichment"),
|
| 84 |
+
input_types=("differential_expression_table",),
|
| 85 |
+
target_output="kegg_enrichment_table",
|
| 86 |
+
),
|
| 87 |
+
WorkflowPrior(
|
| 88 |
+
name="comparative_result_synthesis",
|
| 89 |
+
capabilities=frozenset({"format_conversion", "statistics"}),
|
| 90 |
+
datatypes=frozenset({"kegg_enrichment_table", "merged_pathway_table", "csv"}),
|
| 91 |
+
keyword_hints=frozenset({"compare", "comparative", "shared", "across", "merge", "final table", "csv"}),
|
| 92 |
+
operation_hints=("pathway_merge", "csv_export"),
|
| 93 |
+
input_types=("kegg_enrichment_table",),
|
| 94 |
+
target_output="report_table",
|
| 95 |
+
),
|
| 96 |
+
WorkflowPrior(
|
| 97 |
+
name="variant_interpretation",
|
| 98 |
+
capabilities=frozenset({"variant_calling", "variant_filtering", "annotation"}),
|
| 99 |
+
datatypes=frozenset({"vcf", "annotated_variant_table", "variant_summary"}),
|
| 100 |
+
keyword_hints=frozenset({"variant", "vcf", "mutation", "clinvar", "pathogenic"}),
|
| 101 |
+
operation_hints=("variant_annotation", "variant_summary"),
|
| 102 |
+
input_types=("vcf",),
|
| 103 |
+
target_output="variant_summary",
|
| 104 |
+
),
|
| 105 |
+
WorkflowPrior(
|
| 106 |
+
name="single_cell_interpretation",
|
| 107 |
+
capabilities=frozenset({"single_cell_analysis", "annotation", "statistics"}),
|
| 108 |
+
datatypes=frozenset({"anndata", "marker_gene_table"}),
|
| 109 |
+
keyword_hints=frozenset({"single-cell", "singlecell", "scrna", "cluster", "cell type", "marker"}),
|
| 110 |
+
operation_hints=("single_cell_clustering", "marker_gene_detection"),
|
| 111 |
+
input_types=("anndata",),
|
| 112 |
+
target_output="marker_gene_table",
|
| 113 |
+
),
|
| 114 |
+
WorkflowPrior(
|
| 115 |
+
name="taxonomic_profiling",
|
| 116 |
+
capabilities=frozenset({"taxonomic_classification"}),
|
| 117 |
+
datatypes=frozenset({"metagenomic_reads", "taxonomic_profile"}),
|
| 118 |
+
keyword_hints=frozenset({"metagenomic", "metagenomics", "taxonomy", "taxonomic", "kraken", "metaphlan"}),
|
| 119 |
+
operation_hints=("taxonomic_classification",),
|
| 120 |
+
input_types=("metagenomic_reads",),
|
| 121 |
+
target_output="taxonomic_profile",
|
| 122 |
+
),
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def rank_workflow_priors(
|
| 127 |
+
priors: Iterable[WorkflowPrior],
|
| 128 |
+
*,
|
| 129 |
+
text: str,
|
| 130 |
+
categories: set[str],
|
| 131 |
+
capabilities: set[str],
|
| 132 |
+
datatypes: set[str],
|
| 133 |
+
operations: set[str],
|
| 134 |
+
) -> list[tuple[WorkflowPrior, float]]:
|
| 135 |
+
ranked = []
|
| 136 |
+
for prior in priors:
|
| 137 |
+
score = prior.score(
|
| 138 |
+
text=text,
|
| 139 |
+
categories=categories,
|
| 140 |
+
capabilities=capabilities,
|
| 141 |
+
datatypes=datatypes,
|
| 142 |
+
operations=operations,
|
| 143 |
+
)
|
| 144 |
+
if score >= prior.min_score:
|
| 145 |
+
ranked.append((prior, score))
|
| 146 |
+
ranked.sort(key=lambda item: (-item[1], item[0].name))
|
| 147 |
+
return ranked
|
Biomanus_upload/biomni/know_how/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Know-How library for Biomni agent.
|
| 2 |
+
|
| 3 |
+
This module provides functionality to load and manage know-how documents
|
| 4 |
+
that contain best practices, protocols, and troubleshooting guides.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from .loader import KnowHowLoader
|
| 8 |
+
|
| 9 |
+
__all__ = ["KnowHowLoader"]
|
Biomanus_upload/biomni/know_how/loader.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Loader for know-how documents."""
|
| 2 |
+
|
| 3 |
+
import glob
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class KnowHowLoader:
|
| 9 |
+
"""Load and manage know-how documents for the agent."""
|
| 10 |
+
|
| 11 |
+
def __init__(self, know_how_dir: str | None = None):
|
| 12 |
+
"""Initialize the know-how loader.
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
know_how_dir: Directory containing know-how documents.
|
| 16 |
+
If None, uses the default know_how directory.
|
| 17 |
+
|
| 18 |
+
"""
|
| 19 |
+
if know_how_dir is None:
|
| 20 |
+
# Default to the know_how directory in the package
|
| 21 |
+
current_dir = Path(__file__).parent
|
| 22 |
+
know_how_dir = str(current_dir)
|
| 23 |
+
|
| 24 |
+
self.know_how_dir = know_how_dir
|
| 25 |
+
self.documents = {}
|
| 26 |
+
self._load_documents()
|
| 27 |
+
|
| 28 |
+
def _load_documents(self):
|
| 29 |
+
"""Load all markdown documents from the know-how directory."""
|
| 30 |
+
pattern = os.path.join(self.know_how_dir, "*.md")
|
| 31 |
+
md_files = glob.glob(pattern)
|
| 32 |
+
|
| 33 |
+
for filepath in md_files:
|
| 34 |
+
filename = os.path.basename(filepath)
|
| 35 |
+
filename_without_ext = os.path.splitext(filename)[0]
|
| 36 |
+
|
| 37 |
+
# Skip README, QUICK_START, and other meta documentation (all caps filenames)
|
| 38 |
+
if filename.upper() in ["README.MD", "QUICK_START.MD"] or filename_without_ext.isupper():
|
| 39 |
+
continue
|
| 40 |
+
|
| 41 |
+
# Read the document
|
| 42 |
+
with open(filepath) as f:
|
| 43 |
+
content = f.read()
|
| 44 |
+
|
| 45 |
+
# Extract title, description, and metadata from the document
|
| 46 |
+
title, description, metadata = self._extract_metadata(content, filename)
|
| 47 |
+
|
| 48 |
+
# Use short_description from metadata if available, otherwise fall back to extracted description
|
| 49 |
+
if "short_description" in metadata and metadata["short_description"]:
|
| 50 |
+
description = metadata["short_description"]
|
| 51 |
+
|
| 52 |
+
# Store document info
|
| 53 |
+
doc_id = os.path.splitext(filename)[0]
|
| 54 |
+
self.documents[doc_id] = {
|
| 55 |
+
"id": doc_id,
|
| 56 |
+
"name": title,
|
| 57 |
+
"description": description,
|
| 58 |
+
"content": content,
|
| 59 |
+
"content_without_metadata": self._strip_metadata(content),
|
| 60 |
+
"filepath": filepath,
|
| 61 |
+
"metadata": metadata,
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
def _extract_metadata(self, content: str, filename: str) -> tuple[str, str, dict]:
|
| 65 |
+
"""Extract title, description, and metadata from markdown content.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
content: Markdown content
|
| 69 |
+
filename: Filename (used as fallback for title)
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
Tuple of (title, description, metadata_dict)
|
| 73 |
+
|
| 74 |
+
"""
|
| 75 |
+
lines = content.split("\n")
|
| 76 |
+
|
| 77 |
+
# Extract title (first h1)
|
| 78 |
+
title = None
|
| 79 |
+
for line in lines:
|
| 80 |
+
if line.startswith("# "):
|
| 81 |
+
title = line[2:].strip()
|
| 82 |
+
break
|
| 83 |
+
|
| 84 |
+
if title is None:
|
| 85 |
+
# Fallback to filename
|
| 86 |
+
title = filename.replace("_", " ").replace(".md", "").title()
|
| 87 |
+
|
| 88 |
+
# Extract metadata section
|
| 89 |
+
metadata = {}
|
| 90 |
+
in_metadata = False
|
| 91 |
+
current_field = None
|
| 92 |
+
|
| 93 |
+
for _i, line in enumerate(lines):
|
| 94 |
+
if line.startswith("## Metadata"):
|
| 95 |
+
in_metadata = True
|
| 96 |
+
continue
|
| 97 |
+
elif in_metadata:
|
| 98 |
+
if line.startswith("##") and "Metadata" not in line:
|
| 99 |
+
# End of metadata section
|
| 100 |
+
break
|
| 101 |
+
elif line.startswith("**") and "**:" in line:
|
| 102 |
+
# New metadata field (e.g., **Authors**:)
|
| 103 |
+
field_match = line.split("**")[1]
|
| 104 |
+
current_field = field_match.lower().replace(" ", "_")
|
| 105 |
+
# Get the value after the colon if it exists on the same line
|
| 106 |
+
colon_idx = line.find("**:")
|
| 107 |
+
if colon_idx != -1:
|
| 108 |
+
value_part = line[colon_idx + 3 :].strip()
|
| 109 |
+
if value_part:
|
| 110 |
+
metadata[current_field] = value_part
|
| 111 |
+
else:
|
| 112 |
+
metadata[current_field] = ""
|
| 113 |
+
elif current_field and line.strip() and not line.startswith("---"):
|
| 114 |
+
# Continuation of current field
|
| 115 |
+
if current_field not in metadata:
|
| 116 |
+
metadata[current_field] = ""
|
| 117 |
+
if line.startswith("- "):
|
| 118 |
+
# List item
|
| 119 |
+
if metadata[current_field]:
|
| 120 |
+
metadata[current_field] += ", " + line[2:].strip()
|
| 121 |
+
else:
|
| 122 |
+
metadata[current_field] = line[2:].strip()
|
| 123 |
+
elif not line.startswith("```"):
|
| 124 |
+
# Regular text
|
| 125 |
+
if metadata[current_field]:
|
| 126 |
+
metadata[current_field] += " " + line.strip()
|
| 127 |
+
else:
|
| 128 |
+
metadata[current_field] = line.strip()
|
| 129 |
+
|
| 130 |
+
# Extract description (content under ## Overview or first paragraph)
|
| 131 |
+
description = ""
|
| 132 |
+
in_overview = False
|
| 133 |
+
overview_lines = []
|
| 134 |
+
|
| 135 |
+
for _i, line in enumerate(lines):
|
| 136 |
+
if line.startswith("## Overview"):
|
| 137 |
+
in_overview = True
|
| 138 |
+
continue
|
| 139 |
+
elif in_overview:
|
| 140 |
+
if line.startswith("##"):
|
| 141 |
+
# End of overview section
|
| 142 |
+
break
|
| 143 |
+
elif line.strip():
|
| 144 |
+
overview_lines.append(line.strip())
|
| 145 |
+
|
| 146 |
+
if overview_lines:
|
| 147 |
+
description = " ".join(overview_lines)
|
| 148 |
+
else:
|
| 149 |
+
# Fallback: get first non-empty paragraph after title
|
| 150 |
+
found_title = False
|
| 151 |
+
for line in lines:
|
| 152 |
+
if line.startswith("# "):
|
| 153 |
+
found_title = True
|
| 154 |
+
continue
|
| 155 |
+
if found_title and line.strip() and not line.startswith("#"):
|
| 156 |
+
description = line.strip()
|
| 157 |
+
break
|
| 158 |
+
|
| 159 |
+
# Limit description length
|
| 160 |
+
if len(description) > 200:
|
| 161 |
+
description = description[:197] + "..."
|
| 162 |
+
|
| 163 |
+
return title, description, metadata
|
| 164 |
+
|
| 165 |
+
def _strip_metadata(self, content: str) -> str:
|
| 166 |
+
"""Strip the metadata section from document content.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
content: Full document content with metadata
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
Content without metadata section
|
| 173 |
+
|
| 174 |
+
"""
|
| 175 |
+
lines = content.split("\n")
|
| 176 |
+
result_lines = []
|
| 177 |
+
in_metadata = False
|
| 178 |
+
skip_until_separator = False
|
| 179 |
+
found_first_h1 = False
|
| 180 |
+
|
| 181 |
+
for line in lines:
|
| 182 |
+
# Track first H1 (title)
|
| 183 |
+
if line.startswith("# ") and not found_first_h1:
|
| 184 |
+
result_lines.append(line)
|
| 185 |
+
found_first_h1 = True
|
| 186 |
+
continue
|
| 187 |
+
|
| 188 |
+
# Detect metadata section start
|
| 189 |
+
if line.startswith("## Metadata"):
|
| 190 |
+
in_metadata = True
|
| 191 |
+
continue
|
| 192 |
+
|
| 193 |
+
# Skip separator lines before and after metadata
|
| 194 |
+
if line.strip() == "---":
|
| 195 |
+
if not in_metadata:
|
| 196 |
+
# This might be the separator before metadata
|
| 197 |
+
skip_until_separator = True
|
| 198 |
+
continue
|
| 199 |
+
else:
|
| 200 |
+
# This is the separator after metadata
|
| 201 |
+
in_metadata = False
|
| 202 |
+
skip_until_separator = False
|
| 203 |
+
continue
|
| 204 |
+
|
| 205 |
+
# Skip lines in metadata section
|
| 206 |
+
if in_metadata or skip_until_separator:
|
| 207 |
+
# Check if we hit another H2 (end of metadata)
|
| 208 |
+
if line.startswith("##") and "Metadata" not in line:
|
| 209 |
+
in_metadata = False
|
| 210 |
+
skip_until_separator = False
|
| 211 |
+
result_lines.append(line)
|
| 212 |
+
continue
|
| 213 |
+
|
| 214 |
+
# Keep all other lines
|
| 215 |
+
result_lines.append(line)
|
| 216 |
+
|
| 217 |
+
# Join and clean up extra blank lines
|
| 218 |
+
result = "\n".join(result_lines)
|
| 219 |
+
|
| 220 |
+
# Remove excessive blank lines (more than 2 consecutive)
|
| 221 |
+
while "\n\n\n\n" in result:
|
| 222 |
+
result = result.replace("\n\n\n\n", "\n\n\n")
|
| 223 |
+
|
| 224 |
+
return result.strip()
|
| 225 |
+
|
| 226 |
+
def get_all_documents(self) -> list[dict]:
|
| 227 |
+
"""Get all know-how documents as a list.
|
| 228 |
+
|
| 229 |
+
Returns:
|
| 230 |
+
List of document dictionaries with keys: id, name, description, content
|
| 231 |
+
|
| 232 |
+
"""
|
| 233 |
+
return list(self.documents.values())
|
| 234 |
+
|
| 235 |
+
def get_document_by_id(self, doc_id: str) -> dict | None:
|
| 236 |
+
"""Get a specific know-how document by ID.
|
| 237 |
+
|
| 238 |
+
Args:
|
| 239 |
+
doc_id: Document identifier
|
| 240 |
+
|
| 241 |
+
Returns:
|
| 242 |
+
Document dictionary or None if not found
|
| 243 |
+
|
| 244 |
+
"""
|
| 245 |
+
return self.documents.get(doc_id)
|
| 246 |
+
|
| 247 |
+
def get_document_summaries(self) -> list[dict]:
|
| 248 |
+
"""Get summaries of all documents (without full content).
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
List of document summaries with keys: id, name, description
|
| 252 |
+
|
| 253 |
+
"""
|
| 254 |
+
return [
|
| 255 |
+
{"id": doc["id"], "name": doc["name"], "description": doc["description"]} for doc in self.documents.values()
|
| 256 |
+
]
|
| 257 |
+
|
| 258 |
+
def add_custom_document(self, doc_id: str, name: str, description: str, content: str, metadata: dict | None = None):
|
| 259 |
+
"""Add a custom know-how document programmatically.
|
| 260 |
+
|
| 261 |
+
Args:
|
| 262 |
+
doc_id: Unique identifier for the document
|
| 263 |
+
name: Document title
|
| 264 |
+
description: Brief description
|
| 265 |
+
content: Full document content
|
| 266 |
+
metadata: Optional metadata dictionary (authors, license, etc.)
|
| 267 |
+
|
| 268 |
+
"""
|
| 269 |
+
if metadata is None:
|
| 270 |
+
metadata = {}
|
| 271 |
+
|
| 272 |
+
self.documents[doc_id] = {
|
| 273 |
+
"id": doc_id,
|
| 274 |
+
"name": name,
|
| 275 |
+
"description": description,
|
| 276 |
+
"content": content,
|
| 277 |
+
"filepath": None,
|
| 278 |
+
"metadata": metadata,
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
def get_document_metadata(self, doc_id: str) -> dict | None:
|
| 282 |
+
"""Get metadata for a specific document.
|
| 283 |
+
|
| 284 |
+
Args:
|
| 285 |
+
doc_id: Document identifier
|
| 286 |
+
|
| 287 |
+
Returns:
|
| 288 |
+
Metadata dictionary or None if not found
|
| 289 |
+
|
| 290 |
+
"""
|
| 291 |
+
doc = self.documents.get(doc_id)
|
| 292 |
+
return doc.get("metadata", {}) if doc else None
|
| 293 |
+
|
| 294 |
+
def print_document_info(self, doc_id: str):
|
| 295 |
+
"""Print formatted information about a document including metadata.
|
| 296 |
+
|
| 297 |
+
Args:
|
| 298 |
+
doc_id: Document identifier
|
| 299 |
+
|
| 300 |
+
"""
|
| 301 |
+
doc = self.documents.get(doc_id)
|
| 302 |
+
if not doc:
|
| 303 |
+
print(f"Document '{doc_id}' not found")
|
| 304 |
+
return
|
| 305 |
+
|
| 306 |
+
print("=" * 70)
|
| 307 |
+
print(f"📚 {doc['name']}")
|
| 308 |
+
print("=" * 70)
|
| 309 |
+
print(f"\nDescription: {doc['description']}")
|
| 310 |
+
|
| 311 |
+
metadata = doc.get("metadata", {})
|
| 312 |
+
if metadata:
|
| 313 |
+
print("\n" + "-" * 70)
|
| 314 |
+
print("METADATA")
|
| 315 |
+
print("-" * 70)
|
| 316 |
+
|
| 317 |
+
# Display key metadata fields
|
| 318 |
+
if "authors" in metadata:
|
| 319 |
+
print(f"Authors: {metadata['authors']}")
|
| 320 |
+
if "affiliations" in metadata:
|
| 321 |
+
print(f"Affiliations: {metadata['affiliations']}")
|
| 322 |
+
if "version" in metadata:
|
| 323 |
+
print(f"Version: {metadata['version']}")
|
| 324 |
+
if "last_updated" in metadata:
|
| 325 |
+
print(f"Last Updated: {metadata['last_updated']}")
|
| 326 |
+
if "license" in metadata:
|
| 327 |
+
print(f"License: {metadata['license']}")
|
| 328 |
+
if "commercial_use" in metadata:
|
| 329 |
+
print(f"Commercial Use: {metadata['commercial_use']}")
|
| 330 |
+
if "status" in metadata:
|
| 331 |
+
print(f"Status: {metadata['status']}")
|
| 332 |
+
|
| 333 |
+
print("=" * 70)
|
| 334 |
+
|
| 335 |
+
def remove_document(self, doc_id: str):
|
| 336 |
+
"""Remove a know-how document.
|
| 337 |
+
|
| 338 |
+
Args:
|
| 339 |
+
doc_id: Document identifier
|
| 340 |
+
|
| 341 |
+
"""
|
| 342 |
+
if doc_id in self.documents:
|
| 343 |
+
del self.documents[doc_id]
|
| 344 |
+
|
| 345 |
+
def reload(self):
|
| 346 |
+
"""Reload all documents from disk."""
|
| 347 |
+
self.documents = {}
|
| 348 |
+
self._load_documents()
|
Biomanus_upload/biomni/know_how/resource/CRISPick_download_links.txt
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8030_ICSASG_v2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230421.txt.gz
|
| 2 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8030_ICSASG_v2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230421.summary.txt
|
| 3 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8030_ICSASG_v2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230421.txt.gz
|
| 4 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8030_ICSASG_v2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230421.summary.txt
|
| 5 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20241114.txt.gz
|
| 6 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20241114.summary.txt
|
| 7 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20241114.txt.gz
|
| 8 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20241114.summary.txt
|
| 9 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20241114.txt.gz
|
| 10 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20241114.summary.txt
|
| 11 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20241114.txt.gz
|
| 12 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20241114.summary.txt
|
| 13 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241114.txt.gz
|
| 14 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241114.summary.txt
|
| 15 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_74940_Otsh_v1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230424.txt.gz
|
| 16 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_74940_Otsh_v1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230424.summary.txt
|
| 17 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_74940_Otsh_v1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230424.txt.gz
|
| 18 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_74940_Otsh_v1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230424.summary.txt
|
| 19 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9913_ARS-UCD1.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230418.txt.gz
|
| 20 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9913_ARS-UCD1.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230418.summary.txt
|
| 21 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9913_ARS-UCD1.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230418.txt.gz
|
| 22 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9913_ARS-UCD1.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230418.summary.txt
|
| 23 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_CanFam3.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220923.txt.gz
|
| 24 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_CanFam3.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220923.summary.txt
|
| 25 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_CanFam3.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220923.txt.gz
|
| 26 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_CanFam3.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220923.summary.txt
|
| 27 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20251031.txt.gz
|
| 28 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20251031.summary.txt
|
| 29 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20240611.txt.gz
|
| 30 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20240611.summary.txt
|
| 31 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20251031.txt.gz
|
| 32 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20251031.summary.txt
|
| 33 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20240611.txt.gz
|
| 34 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20240611.summary.txt
|
| 35 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20251031.txt.gz
|
| 36 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20251031.summary.txt
|
| 37 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20240611.txt.gz
|
| 38 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20240611.summary.txt
|
| 39 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20241031.txt.gz
|
| 40 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20241031.summary.txt
|
| 41 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20240611.txt.gz
|
| 42 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20240611.summary.txt
|
| 43 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20241031.txt.gz
|
| 44 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20241031.summary.txt
|
| 45 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20240611.txt.gz
|
| 46 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20240611.summary.txt
|
| 47 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20251031.txt.gz
|
| 48 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20251031.summary.txt
|
| 49 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20240611.txt.gz
|
| 50 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20240611.summary.txt
|
| 51 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20251031.txt.gz
|
| 52 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20251031.summary.txt
|
| 53 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20240611.txt.gz
|
| 54 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20240611.summary.txt
|
| 55 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20251031.txt.gz
|
| 56 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20251031.summary.txt
|
| 57 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241031.txt.gz
|
| 58 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241031.summary.txt
|
| 59 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240611.txt.gz
|
| 60 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240611.summary.txt
|
| 61 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_7227_BDGP6.32_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20230407.txt.gz
|
| 62 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_7227_BDGP6.32_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20230407.summary.txt
|
| 63 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_7227_BDGP6.32_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20230407.txt.gz
|
| 64 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_7227_BDGP6.32_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20230407.summary.txt
|
| 65 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_59479_mRhiFer1_v1.p_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230427.txt.gz
|
| 66 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_59479_mRhiFer1_v1.p_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230427.summary.txt
|
| 67 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_59479_mRhiFer1_v1.p_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230427.txt.gz
|
| 68 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_59479_mRhiFer1_v1.p_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230427.summary.txt
|
| 69 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20220914.txt.gz
|
| 70 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20220914.summary.txt
|
| 71 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20220914.txt.gz
|
| 72 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20220914.summary.txt
|
| 73 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20220914.txt.gz
|
| 74 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20220914.summary.txt
|
| 75 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20220914.txt.gz
|
| 76 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20220914.summary.txt
|
| 77 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20220914.txt.gz
|
| 78 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20220914.summary.txt
|
| 79 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20220914.txt.gz
|
| 80 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20220914.summary.txt
|
| 81 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220914.txt.gz
|
| 82 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220914.summary.txt
|
| 83 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220914.txt.gz
|
| 84 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220914.summary.txt
|
| 85 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20241107.txt.gz
|
| 86 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20241107.summary.txt
|
| 87 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20241107.txt.gz
|
| 88 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20241107.summary.txt
|
| 89 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20241107.txt.gz
|
| 90 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20241107.summary.txt
|
| 91 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20241107.txt.gz
|
| 92 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20241107.summary.txt
|
| 93 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241107.txt.gz
|
| 94 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241107.summary.txt
|
| 95 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20250806.txt.gz
|
| 96 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20250806.summary.txt
|
| 97 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20250718.txt.gz
|
| 98 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20250718.summary.txt
|
| 99 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20250806.txt.gz
|
| 100 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20250806.summary.txt
|
| 101 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20250718.txt.gz
|
| 102 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20250718.summary.txt
|
| 103 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20250806.txt.gz
|
| 104 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20250806.summary.txt
|
| 105 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20250718.txt.gz
|
| 106 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20250718.summary.txt
|
| 107 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20250806.txt.gz
|
| 108 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20250806.summary.txt
|
| 109 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20250718.txt.gz
|
| 110 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20250718.summary.txt
|
| 111 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20250806.txt.gz
|
| 112 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20250806.summary.txt
|
| 113 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20250718.txt.gz
|
| 114 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20250718.summary.txt
|
| 115 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20250806.txt.gz
|
| 116 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20250806.summary.txt
|
| 117 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20250718.txt.gz
|
| 118 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20250718.summary.txt
|
| 119 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20250806.txt.gz
|
| 120 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20250806.summary.txt
|
| 121 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20250718.txt.gz
|
| 122 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20250718.summary.txt
|
| 123 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20250806.txt.gz
|
| 124 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20250806.summary.txt
|
| 125 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20250718.txt.gz
|
| 126 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20250718.summary.txt
|
| 127 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20250806.txt.gz
|
| 128 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20250806.summary.txt
|
| 129 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20250718.txt.gz
|
| 130 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20250718.summary.txt
|
| 131 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20250806.txt.gz
|
| 132 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20250806.summary.txt
|
| 133 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20250718.txt.gz
|
| 134 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20250718.summary.txt
|
| 135 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20250806.txt.gz
|
| 136 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20250806.summary.txt
|
| 137 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRi_RS3i-Chen2013_NCBI_20250718.txt.gz
|
| 138 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRi_RS3i-Chen2013_NCBI_20250718.summary.txt
|
| 139 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241104.txt.gz
|
| 140 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241104.summary.txt
|
| 141 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241028.txt.gz
|
| 142 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241028.summary.txt
|
| 143 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20250807.txt.gz
|
| 144 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20250807.summary.txt
|
| 145 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20250724.txt.gz
|
| 146 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20250724.summary.txt
|
| 147 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20250807.txt.gz
|
| 148 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20250807.summary.txt
|
| 149 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20250724.txt.gz
|
| 150 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20250724.summary.txt
|
| 151 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20250807.txt.gz
|
| 152 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20250807.summary.txt
|
| 153 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20250724.txt.gz
|
| 154 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20250724.summary.txt
|
| 155 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20250807.txt.gz
|
| 156 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20250807.summary.txt
|
| 157 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20250724.txt.gz
|
| 158 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20250724.summary.txt
|
| 159 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20250807.txt.gz
|
| 160 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20250807.summary.txt
|
| 161 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20250724.txt.gz
|
| 162 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20250724.summary.txt
|
| 163 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20250807.txt.gz
|
| 164 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20250807.summary.txt
|
| 165 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20250724.txt.gz
|
| 166 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20250724.summary.txt
|
| 167 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20250807.txt.gz
|
| 168 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20250807.summary.txt
|
| 169 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20250724.txt.gz
|
| 170 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20250724.summary.txt
|
| 171 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20250807.txt.gz
|
| 172 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20250807.summary.txt
|
| 173 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20250724.txt.gz
|
| 174 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20250724.summary.txt
|
| 175 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20250807.txt.gz
|
| 176 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20250807.summary.txt
|
| 177 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20250724.txt.gz
|
| 178 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20250724.summary.txt
|
| 179 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20250807.txt.gz
|
| 180 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20250807.summary.txt
|
| 181 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20250724.txt.gz
|
| 182 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20250724.summary.txt
|
| 183 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRi_RS3i-Chen2013_NCBI_20250807.txt.gz
|
| 184 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRi_RS3i-Chen2013_NCBI_20250807.summary.txt
|
| 185 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20250724.txt.gz
|
| 186 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20250724.summary.txt
|
| 187 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240614.txt.gz
|
| 188 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240614.summary.txt
|
| 189 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20230403.txt.gz
|
| 190 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20230403.summary.txt
|
| 191 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8022_USDA_OmykA_1.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230419.txt.gz
|
| 192 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8022_USDA_OmykA_1.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230419.summary.txt
|
| 193 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8022_USDA_OmykA_1.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230419.txt.gz
|
| 194 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8022_USDA_OmykA_1.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230419.summary.txt
|
| 195 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20241112.txt.gz
|
| 196 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20241112.summary.txt
|
| 197 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20240131.txt.gz
|
| 198 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20240131.summary.txt
|
| 199 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20241112.txt.gz
|
| 200 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20241112.summary.txt
|
| 201 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20240131.txt.gz
|
| 202 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20240131.summary.txt
|
| 203 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20241112.txt.gz
|
| 204 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20241112.summary.txt
|
| 205 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20240131.txt.gz
|
| 206 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20240131.summary.txt
|
| 207 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20241112.txt.gz
|
| 208 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20241112.summary.txt
|
| 209 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20240131.txt.gz
|
| 210 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20240131.summary.txt
|
| 211 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20241112.txt.gz
|
| 212 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20241112.summary.txt
|
| 213 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20240131.txt.gz
|
| 214 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20240131.summary.txt
|
| 215 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20241112.txt.gz
|
| 216 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20241112.summary.txt
|
| 217 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20240131.txt.gz
|
| 218 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20240131.summary.txt
|
| 219 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20241112.txt.gz
|
| 220 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20241112.summary.txt
|
| 221 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20240131.txt.gz
|
| 222 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20240131.summary.txt
|
| 223 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241112.txt.gz
|
| 224 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241112.summary.txt
|
| 225 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240131.txt.gz
|
| 226 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240131.summary.txt
|
| 227 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20220907.txt.gz
|
| 228 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20220907.summary.txt
|
| 229 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20220907.txt.gz
|
| 230 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20220907.summary.txt
|
| 231 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220907.txt.gz
|
| 232 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220907.summary.txt
|
| 233 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220907.txt.gz
|
| 234 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220907.summary.txt
|
| 235 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9544_Mmul_10_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20241025.txt.gz
|
| 236 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9544_Mmul_10_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20241025.summary.txt
|
| 237 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9544_Mmul_10_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241025.txt.gz
|
| 238 |
+
https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9544_Mmul_10_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241025.summary.txt
|
Biomanus_upload/biomni/know_how/resource/addgene_grna_sequences.csv
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Application,Cas9 Species,Depositor,Plasmid ID,PubMed ID,Target Gene,Target Sequence,Target Species
|
| 2 |
+
cut,S. pyogenes,Goncalves,"<a href=""/58252/"">58252</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24870050/"">24870050</a>",AAVS1,GGGGCCACTAGGGACAGGAT,H. sapiens
|
| 3 |
+
cut,S. pyogenes,Church,"<a href=""/41818/"">41818</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",AAVS1,GGGGCCACTAGGGACAGGAT,H. sapiens
|
| 4 |
+
cut,S. pyogenes,Sabatini,"<a href=""/50662/"">50662</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336569/"">24336569</a>",AAVS1,GGGGCCACTAGGGACAGGAT,H. sapiens
|
| 5 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70661/"">70661</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",AAVS1,GGGGCCACTAGGGACAGGAT,H. sapiens
|
| 6 |
+
cut,S. pyogenes,Church,"<a href=""/41817/"">41817</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",AAVS1,GTCCCCTCCACCCCACAGTG,H. sapiens
|
| 7 |
+
cut,S. pyogenes,Kanemaki,"<a href=""/72833/"">72833</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27052166/"">27052166</a>",AAVS1,GGGGCCACTAGGGACAGGAT,H. sapiens
|
| 8 |
+
cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",AAVS1,TGTCCCTAGTGGCCCCACTG,H. sapiens
|
| 9 |
+
cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",AAVS1,ACAGTGGGGCCACTAGGGAC,H. sapiens
|
| 10 |
+
RNA targeting,S. pyogenes,Yeo,"<a href=""/74705/"">74705</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26997482/"">26997482</a>",ACTB,UGGAGCGAGCAUCCCCCAAA,H. sapiens
|
| 11 |
+
cut,S. pyogenes,Jiang,"<a href=""/62552/"">62552</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25739462/"">25739462</a>",actII-orf4,ATTACCAGGGACCGGAGTTC,S. coelicolor
|
| 12 |
+
cut,S. pyogenes,Zaratiegui,"<a href=""/52225/"">52225</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25352017/"">25352017</a>",ade6+,TCTATTGTTCAGATGCCTCG,S. pombe
|
| 13 |
+
cut,S. pyogenes,Zaratiegui,"<a href=""/52227/"">52227</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25352017/"">25352017</a>",ade6-L469,TCTATTGTTCAGATGCCTTG,S. pombe
|
| 14 |
+
cut,S. pyogenes,Zaratiegui,"<a href=""/52226/"">52226</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25352017/"">25352017</a>",ade6-M210,TCTATTGTTCAGATGCTTCG,S. pombe
|
| 15 |
+
cut,S. pyogenes,Ventura,"<a href=""/64071/"">64071</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25337876/"">25337876</a>",Alk and Eml,,M. musculus
|
| 16 |
+
nick,S. pyogenes,Shaw,"<a href=""/74374/"">74374</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26816379/"">26816379</a>",AMPK alpha 1,GGCTGTCGCCATCTTTCTCC,H. sapiens
|
| 17 |
+
nick,S. pyogenes,Shaw,"<a href=""/74375/"">74375</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26816379/"">26816379</a>",AMPK alpha 1,GAAGATCGGCCACTACATTC,H. sapiens
|
| 18 |
+
nick,S. pyogenes,Shaw,"<a href=""/74376/"">74376</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26816379/"">26816379</a>",AMPK alpha 2,GTCAGCCATCTTCGGCGCGCG,H. sapiens
|
| 19 |
+
nick,S. pyogenes,Shaw,"<a href=""/74377/"">74377</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26816379/"">26816379</a>",AMPK alpha 2,GAAGATCGGACACTACGTGC,H. sapiens
|
| 20 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70660/"">70660</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>","Amplicon, JAK2",GAGGCATATTCTTCTCCTGG,H. sapiens
|
| 21 |
+
cut,S. pyogenes,Voytas,"<a href=""/70019/"">70019</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26541286/"">26541286</a>",ANT1,TGTCGTTTATAATTTGTAGA,S. lycopersicum
|
| 22 |
+
cut,S. pyogenes,Voytas,"<a href=""/70018/"">70018</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26541286/"">26541286</a>",ANT1,ACAATTTAATACACCTTTT,S. lycopersicum
|
| 23 |
+
cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",AOC,CAAAAGACTGTCAATTCCCT,N. attenuata
|
| 24 |
+
cut/nick,S. pyogenes,Yamamoto,"<a href=""/58780/"">58780</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",APC,GAAGCGGGCAAAGGGGCGAC,H. sapiens
|
| 25 |
+
cut,S. pyogenes,Joung,"<a href=""/42241/"">42241</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",apoea,GGATGAGCCAAGAAGCCGCT,D. rerio
|
| 26 |
+
activate,S. pyogenes,Sato,"<a href=""/64131/"">64131</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",ASCL1,TGGATGGAGAGTTTGCAAGGAGC,H. sapiens
|
| 27 |
+
activate,S. pyogenes,Sato,"<a href=""/64130/"">64130</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",ASCL1,TGGGCAGCCGCTCGCTGCAGCAG,H. sapiens
|
| 28 |
+
activate,S. pyogenes,Sato,"<a href=""/64129/"">64129</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",ASCL1,TGGGGCTGGGTGTCCCATTGAAA,H. sapiens
|
| 29 |
+
activate,S. pyogenes,Sato,"<a href=""/64132/"">64132</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",ASCL1,TGGTGTTTATTCAGCCGGGAGTC,H. sapiens
|
| 30 |
+
cut,S. pyogenes,Mashimo,"<a href=""/60968/"">60968</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Asip,ATGACAGGAGTCTAACTCAC,R. norvegicus
|
| 31 |
+
tag,S. pyogenes,Mendenhall,"<a href=""/64690/"">64690</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",ATF1,TAGGAATCAAACACTTTTATTGG,H. sapiens
|
| 32 |
+
cut,S. pyogenes,Yamamoto,"<a href=""/58779/"">58779</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",ATM,TGAATTGGGATGCTGTTTTT,H. sapiens
|
| 33 |
+
cut,S. pyogenes,Mello,"<a href=""/58981/"">58981</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24879462/"">24879462</a>",avr-14,GATTGGAGAGTTAGACCACG,C. elegans
|
| 34 |
+
cut,S. pyogenes,Mello,"<a href=""/58982/"">58982</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24879462/"">24879462</a>",avr-15,GTTTGCAATATAAGTCACCC,C. elegans
|
| 35 |
+
cut,S. pyogenes,Yamamoto,"<a href=""/58782/"">58782</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",AXIN2,TATGTTGGTGACTTGCCTCC,H. sapiens
|
| 36 |
+
methylation,S. pyogenes,Zoldoš,"<a href=""/71828/"">71828</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26969735/"">26969735</a>",BACH2,AATGTAGCGATTGAGAGTGTGGG,H. sapiens
|
| 37 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70658/"">70658</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",BCR/ABL amplicon,GGCTCCCTTCAAGTGGGATG,H. sapiens
|
| 38 |
+
cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",BFP,ATGGCGTGCAGTGCTTCAGC,
|
| 39 |
+
cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",BIN2,ATCACAGTGATGCTCGTCAA,L. sativa
|
| 40 |
+
cut,S. pyogenes,Hanna,"<a href=""/59724/"">59724</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25543152/"">25543152</a>",BLIMP1,CGGATGGGGTAAACGACCCG,H. sapiens
|
| 41 |
+
cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",BRI1,TTTGAAAGATGGAAGCGCGG,A. thaliana
|
| 42 |
+
cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",BRI1,TGAAACTAAACTGGTCCACA,A. thaliana
|
| 43 |
+
interfere,S. pyogenes,Lu,"<a href=""/68892/"">68892</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",BT1754,GAAAATGGGGTGTATCCTGC,B. thetaiotaomicron
|
| 44 |
+
interfere,S. pyogenes,Lu,"<a href=""/68891/"">68891</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",BT1854,ATTGAAGAACAAAAGCAGTT,B. thetaiotaomicron
|
| 45 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70650/"">70650</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C16orf80,CGATGCTGTAGAGGATGGAG,H. sapiens
|
| 46 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70651/"">70651</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C16orf80,TGTCTGAGAAGTAAACCCGT,H. sapiens
|
| 47 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70653/"">70653</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C3orf17,GGGCCAAATGGGGTTTGTGG,H. sapiens
|
| 48 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70652/"">70652</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C3orf17,GTGTGAGAATCCCTAAGGCG,H. sapiens
|
| 49 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70654/"">70654</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C9orf114,CAGGCGGGCTCACCTCCGTG,H. sapiens
|
| 50 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70655/"">70655</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C9orf114,GCGGCAGAGAAGGAGGACCG,H. sapiens
|
| 51 |
+
cut,S. pyogenes,Church,"<a href=""/43803/"">43803</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23460208/"">23460208</a>",CAN1,GATACGTTCTCTATGGAGGA,S. cerevisiae
|
| 52 |
+
cut,S. pyogenes,Bornhauser,"<a href=""/75164/"">75164</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27194728/"">27194728</a>",Caspase-8,GCCTGGACTACATTCCGCAA,H. sapiens
|
| 53 |
+
cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",CCR5,TGACATCAATTATTATACAT,H. sapiens
|
| 54 |
+
interfere,S. pyogenes,Qi,"<a href=""/46918/"">46918</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",CD71,GGACGCGCTAGTGTGAGTGC,H. sapiens
|
| 55 |
+
cut,S. pyogenes,Yamamoto,"<a href=""/58781/"">58781</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",CDH1,TGACTTGCGAGGGACGCATT,H. sapiens
|
| 56 |
+
activate,S. pyogenes,Vale,"<a href=""/60905/"">60905</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25307933/"">25307933</a>",CDKN1B,GAAGCCGGGACCTGGACCAG,H. sapiens
|
| 57 |
+
tag,S. pyogenes,Mendenhall,"<a href=""/64036/"">64036</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",CEBPB,CTCCGGCCACTGCTAGCGCGG,H. sapiens
|
| 58 |
+
tag,S. pyogenes,Mendenhall,"<a href=""/64047/"">64047</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",CEBPB,GCCGGCAGGGGGACGCGCGCGG,H. sapiens
|
| 59 |
+
cut,S. pyogenes,Ikawa,"<a href=""/50718/"">50718</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24284873/"">24284873</a>",Cent1,GGCAACGTTTGACTTCCTGA,M. musculus
|
| 60 |
+
cut,S. pyogenes,Yamamoto,"<a href=""/58783/"">58783</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",CFTR,TCTGTATCTATATTCATCAT,H. sapiens
|
| 61 |
+
tag,S. pyogenes,Mendenhall,"<a href=""/64940/"">64940</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",CREB1,GCCACAAATCAGATTAATTTGG,H. sapiens
|
| 62 |
+
tag,S. pyogenes,Mendenhall,"<a href=""/64939/"">64939</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",CREB1,GCCACAAATCAGATTAATTTGGG,H. sapiens
|
| 63 |
+
cut,S. pyogenes,Hong,"<a href=""/68060/"">68060</a>","<a href=""http://fungalbiolbiotech.biomedcentral.com/articles/10.1186/s40694-015-0015-1"">Fungal Biology and Biotechnology 2015, 2:4</a>",csr-1,GAGTGGGAGGGTCCCGTCCT,N. crassa
|
| 64 |
+
cut,S. pyogenes,Jacks,"<a href=""/59912/"">59912</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25119044/"">25119044</a>",Ctnnb1,AGCTCCTTCCCTGAGTGGCA,M. musculus
|
| 65 |
+
cut,S. pyogenes,Jacks,"<a href=""/59911/"">59911</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25119044/"">25119044</a>",Ctnnb1,CTGTGGTGGTGGCACCAGAA,M. musculus
|
| 66 |
+
interfere,S. pyogenes,Qi,"<a href=""/46917/"">46917</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",CXCR4,GCAGGTAGCAAAGTGACGCCGA,H. sapiens
|
| 67 |
+
cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",CXCR4,CCTCTTTGTCATCACGCTTC,H. sapiens
|
| 68 |
+
activate,S. pyogenes,Lu,"<a href=""/64385/"">64385</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,ACAGAGCACATGCATGCCAT,S. cerevisiae
|
| 69 |
+
activate,S. pyogenes,Lu,"<a href=""/64387/"">64387</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,ACTAATACTTTCAACATTTT,S. cerevisiae
|
| 70 |
+
activate,S. pyogenes,Lu,"<a href=""/64382/"">64382</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,ATATCGAATTCCTGCAGCCC,S. cerevisiae
|
| 71 |
+
activate,S. pyogenes,Lu,"<a href=""/64380/"">64380</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,ATATTCTTTCCTTATACATT,S. cerevisiae
|
| 72 |
+
activate,S. pyogenes,Lu,"<a href=""/64379/"">64379</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,CTAGATATTAAAATGTCTAA,S. cerevisiae
|
| 73 |
+
activate,S. pyogenes,Lu,"<a href=""/64388/"">64388</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,GTTGAAAGTATTAGTTAAAG,S. cerevisiae
|
| 74 |
+
activate,S. pyogenes,Lu,"<a href=""/64381/"">64381</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,TACATACAGTAGGATCCTA,S. cerevisiae
|
| 75 |
+
activate,S. pyogenes,Lu,"<a href=""/64383/"">64383</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,TATAGTAATTTATGCTGCAA,S. cerevisiae
|
| 76 |
+
activate,S. pyogenes,Lu,"<a href=""/64384/"">64384</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,TGCAAAGGTCCTAATGTATA,S. cerevisiae
|
| 77 |
+
activate,S. pyogenes,Lu,"<a href=""/64386/"">64386</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,TGTATGTACATACAGTACCC,S. cerevisiae
|
| 78 |
+
activate,S. pyogenes,Lu,"<a href=""/64378/"">64378</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,TTACTGTATGTACATACAGT,S. cerevisiae
|
| 79 |
+
cut,S. pyogenes,Wente,"<a href=""/47932/"">47932</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23918387/"">23918387</a>",ddx19,GGCAACAGATTCGTGGGCCC,D. rerio
|
| 80 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70656/"">70656</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",DDX3Y,GCAGTTTAGCGATATTGACA,H. sapiens
|
| 81 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70657/"">70657</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",DDX3Y,TCTTGTTGGGGCTAAAACCA,H. sapiens
|
| 82 |
+
cut,S. pyogenes,Church,"<a href=""/41822/"">41822</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",DNMT3A,GAGATGATCGCCCCTTCTTC,H. sapiens
|
| 83 |
+
cut,S. pyogenes,Church,"<a href=""/41821/"">41821</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",DNMT3A,GCATGATGCGCGGCCCAAGG,H. sapiens
|
| 84 |
+
cut,S. pyogenes,Church,"<a href=""/41823/"">41823</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",DNMT3B,GAATTACTCACGCCCCAAGG,H. sapiens
|
| 85 |
+
cut,VRER Cas9 mutant,Fire,"<a href=""/71516/"">71516</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26680661/"">26680661</a>",dpy-10,CGCTACCATAGGCACCACG,C. elegans
|
| 86 |
+
cut,S. pyogenes,Katic,"<a href=""/65630/"">65630</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26044730/"">26044730</a>",dpy-10,GCTACCATAGGCACCACGAG,C.elegans
|
| 87 |
+
cut,S. pyogenes,Fire,"<a href=""/59933/"">59933</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",dpy-10,GCTACCATAGGCACCACGAG,C. elegans
|
| 88 |
+
cut,VQR Cas9 variant,Fire,"<a href=""/71479/"">71479</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26680661/"">26680661</a>",dpy-10,TCCGCTACCATAGGCACCA,C. elegans
|
| 89 |
+
cut,S. pyogenes,Seydoux,"<a href=""/70047/"">70047</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26187122/"">26187122</a>",dpy‐10,GCTACCATAGGCACCACGAG,C. elegans
|
| 90 |
+
cut,S. pyogenes,Joung,"<a href=""/42242/"">42242</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",drd3,GGAAACTACAGCCCAGCGTC,D. rerio
|
| 91 |
+
cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",DWD1,TGCATCGTCCAAGCGCACAG,O. sativa
|
| 92 |
+
cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",DWD1,CTACGACGTCAGGTTCTACC,O. sativa
|
| 93 |
+
cut,S. pyogenes,Christiaen,"<a href=""/59990/"">59990</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25336740/"">25336740</a>",Ebf,GCTGAGGGTTGGACAACAGG,C. intestinalis
|
| 94 |
+
dCas9-FokI,S. pyogenes,Joung,"<a href=""/60071/"">60071</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24770325/"">24770325</a>",EGFP,"multiple, see article",A. victoria
|
| 95 |
+
cut,S. pyogenes,Zhang,"<a href=""/51764/"">51764</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GAAGTTCGAGGGCGACACCC,A. victoria
|
| 96 |
+
cut,S. pyogenes,Zhang,"<a href=""/51761/"">51761</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GAGCTGGACGGCGACGTAAA,A. victoria
|
| 97 |
+
cut,S. pyogenes,Joung,"<a href=""/47512/"">47512</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",EGFP,GATGCCGTTCTTCTGCTTGT,A. victoria
|
| 98 |
+
cut,S. pyogenes,Zhang,"<a href=""/51763/"">51763</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GGAGCGCACCATCTTCTTCA,A. victoria
|
| 99 |
+
cut,S. pyogenes,Zhang,"<a href=""/51762/"">51762</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GGCCACAAGTTCAGCGTGTC,A. victoria
|
| 100 |
+
cut,S. pyogenes,Del Bene,"<a href=""/61051/"">61051</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24179142/"">24179142</a>",EGFP,GGCGAGGGCGATGCCACCTA,A. victoria
|
| 101 |
+
cut,S. pyogenes,Chen,"<a href=""/46760/"">46760</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23918387/"">23918387</a>",EGFP,GGGCACGGGCAGCTTGCCGG,A. victoria
|
| 102 |
+
cut,S. pyogenes,Joung,"<a href=""/47511/"">47511</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",EGFP,GGGCACGGGCAGCTTGCCGG,A. victoria
|
| 103 |
+
cut,S. pyogenes,Zhang,"<a href=""/51760/"">51760</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GGGCGAGGAGCTGTTCACCG,A. victoria
|
| 104 |
+
cut,S. pyogenes,Zhang,"<a href=""/51765/"">51765</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GGTGAACCGCATCGAGCTGA,A. victoria
|
| 105 |
+
cut,S. pyogenes,Joung,"<a href=""/47513/"">47513</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",EGFP,GGTGGTGCAGATGAACTTCA,A. victoria
|
| 106 |
+
cut,S. pyogenes,Goncalves,"<a href=""/58255/"">58255</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24870050/"">24870050</a>",EGFP,TGAAGAAGATGGTGCGCTC,A. victoria
|
| 107 |
+
cut,S. pyogenes,Moffat,"<a href=""/74187/"">74187</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",EIF3D,TGTAGGTTGCCTCCATGGCC,H. sapiens
|
| 108 |
+
cut,S. pyogenes,Moffat,"<a href=""/74188/"">74188</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",EIF3D,AGACGACCCTGTCATCCGCA,H. sapiens
|
| 109 |
+
cut,S. pyogenes,Zhang,"<a href=""/42337/"">42337</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287718/"">23287718</a>",Emx1,,H. sapiens
|
| 110 |
+
cut,S. pyogenes,Joung,"<a href=""/47508/"">47508</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",EMX1,GAGTCCGAGCAGAAGAAGAA,H. sapiens
|
| 111 |
+
cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",EMX1,CGATGTCACCTCCAATGACT,H. sapiens
|
| 112 |
+
cut,S. pyogenes,Chen,"<a href=""/71288/"">71288</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26193878/"">26193878</a>","ETC2, TRY, and CPC",,A. thaliana
|
| 113 |
+
cut,S. pyogenes,Bornhauser,"<a href=""/75166/"">75166</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27194728/"">27194728</a>",FADD,ACTCCCGCACACGCTCTGTC,H. sapiens
|
| 114 |
+
cut,S. pyogenes,Joung,"<a href=""/47510/"">47510</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",FANCF,GGAATCCCTTCTGCAGCACC,H. sapiens
|
| 115 |
+
cut,S. pyogenes,Seydoux,"<a href=""/65597/"">65597</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",fbf-2,GTAGTCACGGCGATGATTA,C. elegans
|
| 116 |
+
cut,S. pyogenes,Seydoux,"<a href=""/65591/"">65591</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",fbf-2,TAATCATCGCCGTGACTAC,C. elegans
|
| 117 |
+
cut,S. pyogenes,Joung,"<a href=""/42244/"">42244</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",fh,GGAGCGAGCGGAGCGGTACA,D. rerio
|
| 118 |
+
cut,S. pyogenes,Joung,"<a href=""/42243/"">42243</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",fh,GGAGCGGTACATGGCGACCG,D. rerio
|
| 119 |
+
tag,S. pyogenes,Mendenhall,"<a href=""/64254/"">64254</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",GABPA,GAAAAGGATAATTGAGCCCCAGG,H. sapiens
|
| 120 |
+
tag,S. pyogenes,Mendenhall,"<a href=""/64255/"">64255</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",GABPA,TTTGGAGTCTCAGAATGTCCTGG,H. sapiens
|
| 121 |
+
activate,S. pyogenes,Qi,"<a href=""/46916/"">46916</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",GAL4 UAS,GAACGACTAGTTAGGCGTGTA,
|
| 122 |
+
activate,S. pyogenes,Qi,"<a href=""/46915/"">46915</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",GAL4 UAS,GTTGGAGCACTGTCCTCCGAACGT,
|
| 123 |
+
activate,S. pyogenes,Sato,"<a href=""/64158/"">64158</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",GAL4UAS,TGGGGACAGTACTCCGCTCGAGT,
|
| 124 |
+
activate,S. pyogenes,Sato,"<a href=""/64157/"">64157</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",GAL4UAS,TGGGTCTTCGGAGGACAGTACTC,
|
| 125 |
+
activate,S. pyogenes,Sato,"<a href=""/64159/"">64159</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",GAL4UAS,TGGTCCGTCTAGAAACTCGGTAC,
|
| 126 |
+
cut,S. pyogenes,Du,"<a href=""/65566/"">65566</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25849248/"">25849248</a>",gfap,GTGCGCAACACATAGCACCA,D. rerio
|
| 127 |
+
interfere,S. pyogenes,Qi,"<a href=""/46914/"">46914</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",GFP,GAATAGCTCAGAGGCCGAGG,A. victoria
|
| 128 |
+
cut,S. pyogenes,Church,"<a href=""/41820/"">41820</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",GFP,GGAGCGCACCATCTTCTTCA,A. victoria
|
| 129 |
+
cut,S. pyogenes,Church,"<a href=""/41819/"">41819</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",GFP,GTGAACCGCATCGAGCTGAA,A. victoria
|
| 130 |
+
cut,S. pyogenes,"Guigo, Johnson","<a href=""/72619/"">72619</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",GFP,gRNA1: GAGCTGGACGGCGACGTAAA; gRNA2: CAGAACACCCCCATCGGCGA,Synthetic
|
| 131 |
+
interfere,S. pyogenes,Hudson,"<a href=""/73221/"">73221</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26689101/"">26689101</a>",GFPmut3b,ACCATCTAATTCAACAAGAATT,A. victoria
|
| 132 |
+
interfere,S. pyogenes,Hudson,"<a href=""/73224/"">73224</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26689101/"">26689101</a>",GFPmut3b,ACCATCTAATTCAACAAGAATT,synthetic
|
| 133 |
+
CRISPR-display,S. pyogenes,Rinn,"<a href=""/68422/"">68422</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26030444/"">26030444</a>",GLuc,GATCTAGATACGACTCACTAT,synthetic
|
| 134 |
+
cut,S. pyogenes,Ye,"<a href=""/61833/"">61833</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24424410/"">24424410</a>",gp78,GCCCAGCCTCCGCACCTACA,H. sapiens
|
| 135 |
+
cut,S. pyogenes,Joung,"<a href=""/42245/"">42245</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",gsk3b,GGGACCTGACCGGCCGCAGG,D. rerio
|
| 136 |
+
cut,S. pyogenes,Jin,"<a href=""/64333/"">64333</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25281382/"">25281382</a>",his3,ATTGCGATCTCTTTAAAGGG,S. cerevisiae
|
| 137 |
+
cut,S. pyogenes,Yan,"<a href=""/69539/"">69539</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26130722/"">26130722</a>",HPRT,GACTGTAAGTGAATTACTT,H. sapiens
|
| 138 |
+
cut,S. pyogenes,Yamamoto,"<a href=""/58778/"">58778</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",HPRT1,CTGATAAAATCTACAGTCAT,H. sapiens
|
| 139 |
+
cut,S. pyogenes,Yamamoto,"<a href=""/58771/"">58771</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",HPRT1,TTATGCTGAGGATTTGGAAA,H. sapiens
|
| 140 |
+
activate,S. pyogenes,Sato,"<a href=""/64142/"">64142</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",IL1RN,TGGACGCAGATAAGAACCAGTT,H. sapiens
|
| 141 |
+
activate,S. pyogenes,Sato,"<a href=""/64151/"">64151</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",IL1RN,TGGCATCAAGTCAGCCATCAGC,H. sapiens
|
| 142 |
+
activate,S. pyogenes,Sato,"<a href=""/64152/"">64152</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",IL1RN,TGGGAGTCACCCTCCTGGAAAC,H. sapiens
|
| 143 |
+
activate,S. pyogenes,Sato,"<a href=""/64140/"">64140</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",IL1RN,TGGTGTACTCTCTGAGGTGCTC,H. sapiens
|
| 144 |
+
cut,S. pyogenes,Xue,"<a href=""/66582/"">66582</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",inverted GFP,GAGCGGCCGCTCGAGTCTAG,A. victoria
|
| 145 |
+
cut,S. pyogenes,Xue,"<a href=""/66583/"">66583</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",inverted GFP,GATATCGAATTCCTGCAGCC,A. victoria
|
| 146 |
+
cut,S. pyogenes,Xue,"<a href=""/66581/"">66581</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",inverted GFP,GTATCGATACCGTCGACCTCG,A. victoria
|
| 147 |
+
cut,S. pyogenes,Xue,"<a href=""/66584/"">66584</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",inverted GFP,GTTGATCCATAACTTCGTAT,A. victoria
|
| 148 |
+
purify,S. pyogenes,Fujii,"<a href=""/61079/"">61079</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23942116/"">23942116</a>",IRF1,CCGGGGGCGCTGGGCTGTCCCGG,H. sapiens
|
| 149 |
+
purify,S. pyogenes,Fujii,"<a href=""/61080/"">61080</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25051498/"">25051498</a>",IRF1,GCGCTTACACTTTAGGAGACACTC,H. sapiens
|
| 150 |
+
cut,S. pyogenes,Sabatini,"<a href=""/70679/"">70679</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",JAK2 amplicon ,GGTTTAATGGAAGAGAAGGG,H. sapiens
|
| 151 |
+
cut,S. pyogenes,Seydoux,"<a href=""/66085/"">66085</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",K08F4.2,AATCACTCCCTGTTTGTGT,C. elegans
|
| 152 |
+
cut,S. pyogenes,Seydoux,"<a href=""/66095/"">66095</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",K08F4.2,CACGAGGTGGTATGCGCAG,C. elegans
|
| 153 |
+
cut,S. pyogenes,Seydoux,"<a href=""/66092/"">66092</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",K08F4.2,CGCAGCGGTTTCCAAAATG,C. elegans
|
| 154 |
+
cut,S. pyogenes,Seydoux,"<a href=""/66087/"">66087</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",K08F4.2,GCCTTAACCCAGAATAAGA,C. elegans
|
| 155 |
+
cut,S. pyogenes,Seydoux,"<a href=""/66089/"">66089</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",K08F4.2,TATTAAATGCAGATAACCT,C. elegans
|
| 156 |
+
cut,S. pyogenes,Wolfe,"<a href=""/69238/"">69238</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",KANK3,GCATGGGTGATGTCAATGCC,H. sapiens
|
| 157 |
+
cut,S. pyogenes,Mashimo,"<a href=""/60969/"">60969</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Kit-1,CATCTGTGCGGCCGTTGGCT,R. norvegicus
|
| 158 |
+
cut,S. pyogenes,Mashimo,"<a href=""/60970/"">60970</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Kit-2,CTAACGTTCCAGCGCTCGTT,R. norvegicus
|
| 159 |
+
cut,S. pyogenes,Mashimo,"<a href=""/60971/"">60971</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Kit-2,GTCAAGATGTCATCTTACGG,R. norvegicus
|
| 160 |
+
cut,S. pyogenes,Calarco,"<a href=""/46170/"">46170</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23817069/"">23817069</a>",klp-12,GATCCACAAGTTACAATTGG,C. elegans
|
| 161 |
+
cut,S. pyogenes,Zhang,"<a href=""/60224/"">60224</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25263330/"">25263330</a>","Kras, p53, and Lkb1","multiple, see article",M. musculus
|
| 162 |
+
cut,S. pyogenes,Zhang,"<a href=""/60225/"">60225</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25263330/"">25263330</a>",LacZ,TGCGAATACGCCCACGCGAT,E. coli
|
| 163 |
+
cut,S. pyogenes,Moffat,"<a href=""/74179/"">74179</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",LacZ,CCCGAATCTCTATCGTGCGG,E.coli
|
| 164 |
+
cut,S. pyogenes,Patron,"<a href=""/68256/"">68256</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26616834/"">26616834</a>",lC.GA4a,GTTGAGAGGGGAGCCGGTGA,B. oleracea
|
| 165 |
+
cut,S. pyogenes,Patron,"<a href=""/68255/"">68255</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26616834/"">26616834</a>",lC.GA4a,GTTTTCACTTGCGGCCGGAG,B. oleracea
|
| 166 |
+
cut,S. pyogenes,Jin,"<a href=""/64332/"">64332</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25281382/"">25281382</a>",leu2,AATCACAGCCGAAGCCATTA,S. cerevisiae
|
| 167 |
+
cut,S. pyogenes,Winslow,"<a href=""/66894/"">66894</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26178787/"">26178787</a>",Lkb1,GTGGTGGGCCGCAGTCACAA,M. musculus
|
| 168 |
+
cut,S. pyogenes,Xue,"<a href=""/66586/"">66586</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",Lox-STOP-Lox,GCGTATAGCATACATTATACG,synthetic
|
| 169 |
+
cut,S. pyogenes,Xue,"<a href=""/66585/"">66585</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",Lox-STOP-Lox,GCGTATAGTACACATTATACG,synthetic
|
| 170 |
+
cut,S. pyogenes,Wolfe,"<a href=""/69237/"">69237</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",LRTM2,GCTGGCGGAAGACAGAGTGC,H. sapiens
|
| 171 |
+
cut,S. pyogenes,Moffat,"<a href=""/74190/"">74190</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",Luciferase,ACAACTTTACCGACCGCGCC,
|
| 172 |
+
cut,S. pyogenes,Cate,"<a href=""/60847/"">60847</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25139909/"">25139909</a>",LYP1,CATAATAACGTCCAATAAAT,S. cerevisiae
|
| 173 |
+
cut,S. pyogenes,"Guigo, Johnson","<a href=""/72620/"">72620</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",Malat1,gRNA1: GAACCGGTGGGGCTGCGTCA; gRNA2: GGCAGGAGAGGCCAGTTGCG,H. sapiens
|
| 174 |
+
cut,S. pyogenes,"Guigo, Johnson","<a href=""/72621/"">72621</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",Malat1,gRNA1: GCAACTTCCATTTTCAGTCT; gRNA2: GGAAGCCTCAGCTCGCCTGA,H. sapiens
|
| 175 |
+
cut,S. pyogenes,"Guigo, Johnson","<a href=""/72622/"">72622</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",Malat1,gRNA1:GCTGGGGCTCAGTTGCGTAA; gRNA2:AGGTTTCTAAAACATGACGG,H. sapiens
|
| 176 |
+
cut,S. pyogenes,"Guigo, Johnson","<a href=""/72623/"">72623</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",Malat1,gRNA1:GTTGAGATGAAGCTTCTTCA; gRNA2:TCAACCGTCCCTGCAAGGCT,H. sapiens
|
| 177 |
+
cut,S. pyogenes,"Guigo, Johnson","<a href=""/72624/"">72624</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",Malat1,gRNA1: GAAACCTCGTGTAGCTATCA; gRNA2: AATGTGAAGGACTTTCGTAA,H. sapiens
|
| 178 |
+
nick,S. pyogenes,Hanna,"<a href=""/61514/"">61514</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25569111/"">25569111</a>",Mettl14,GCCGCTCCCGGATCTCCTGC,M. musculus
|
| 179 |
+
nick,S. pyogenes,Hanna,"<a href=""/61515/"">61515</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25569111/"">25569111</a>",Mettl14,GCGGCAGCTCCTAGCTCAGC,M. musculus
|
| 180 |
+
cut,S. pyogenes,Zhao,"<a href=""/69801/"">69801</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26293209/"">26293209</a>",mitf,CTTTCGGATATAATCCACGG,S. scrofa
|
| 181 |
+
cut,S. pyogenes,Chen,"<a href=""/47931/"">47931</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23918387/"">23918387</a>",mitfa,GGTCTCTCGCAGGATGTTGC,D. rerio
|
| 182 |
+
cut,S. pyogenes,Bleris,"<a href=""/62715/"">62715</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25527740/"">25527740</a>",mKate,ATGAGAATCAAGGCGGTCGA,synthetic
|
| 183 |
+
cut,S. pyogenes,Bornhauser,"<a href=""/75167/"">75167</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27194728/"">27194728</a>",MLKL,ATCCCCGTGGATTCTGCTAA,H. sapiens
|
| 184 |
+
cut,S. pyogenes,Katsanis,"<a href=""/72890/"">72890</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26429889/"">26429889</a>",mmp21,CGGAGCTGATCACTGACA,D. rerio
|
| 185 |
+
visualize,S. pyogenes,Qi,"<a href=""/51025/"">51025</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24360272/"">24360272</a>",MUC4,GTGGCGTGACCTGTGGATGCTG,H. sapiens
|
| 186 |
+
activate,S. pyogenes,Sato,"<a href=""/64138/"">64138</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",MYOD1,TGGCCTCCCTCCCTGCCCGGTAG,H. sapiens
|
| 187 |
+
activate,S. pyogenes,Sato,"<a href=""/64139/"">64139</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",MYOD1,TGGGAGGTTTGGAAAGGGCGTGC,H. sapiens
|
| 188 |
+
activate,S. pyogenes,Sato,"<a href=""/64136/"">64136</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",MYOD1,TGGGCCTGGGCTCCGGGGCGTTT,H. sapiens
|
| 189 |
+
activate,S. pyogenes,Sato,"<a href=""/64137/"">64137</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",MYOD1,TGGGGGCCCCTGCGGCCACCCCG,H. sapiens
|
| 190 |
+
activate,S. pyogenes,Sato,"<a href=""/64156/"">64156</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",NANOG,TGGCATATTCCTGATTTAAAAGT,H. sapiens
|
| 191 |
+
activate,S. pyogenes,Sato,"<a href=""/64153/"">64153</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",NANOG,TGGCGCCAGGAGGGGTGGGTCTA,H. sapiens
|
| 192 |
+
activate,S. pyogenes,Sato,"<a href=""/64154/"">64154</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",NANOG,TGGGCCTTGGTGAGACTGGTAGA,H. sapiens
|
| 193 |
+
activate,S. pyogenes,Sato,"<a href=""/64155/"">64155</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",NANOG,TGGTGTCTTCAGGTTCTGTTGCT,H. sapiens
|
| 194 |
+
interfere,S. pyogenes,Lu,"<a href=""/68897/"">68897</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",NanoLuc,AGCTTACGCCACCCTGTTCC,synthetic
|
| 195 |
+
interfere,S. pyogenes,Lu,"<a href=""/68898/"">68898</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",NanoLuc,GACAGAACGATGCGCTGAAT,synthetic
|
| 196 |
+
interfere,S. pyogenes,Lu,"<a href=""/68895/"">68895</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",NanoLuc,TCACGCTCACACCCAGGTTC,synthetic
|
| 197 |
+
interfere,S. pyogenes,Lu,"<a href=""/68896/"">68896</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",NanoLuc,TTGATCCAAATTATAACCCG,synthetic
|
| 198 |
+
cut,S. pyogenes,Lu,"<a href=""/61270/"">61270</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25240928/"">25240928</a>",NDM-1,GGGCAGTCGCTTCCAACGGTTTGATCGTCA,
|
| 199 |
+
cut,S. pyogenes,Cepko,"<a href=""/69992/"">69992</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25155555/"">25155555</a>",near LoxP sites,CGAAGTTATATTAAGGGTTC,synthetic
|
| 200 |
+
interfere,S. pyogenes,Lu,"<a href=""/68893/"">68893</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",negative control,CTGGAATGAATTGGCCTATG,
|
| 201 |
+
cut,S. pyogenes,Winslow,"<a href=""/66895/"">66895</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26178787/"">26178787</a>",negative control,GCGAGGTATTCGGCTCCGCG,M. musculus
|
| 202 |
+
cut,S. pyogenes,Christiaen,"<a href=""/60006/"">60006</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25336740/"">25336740</a>",negative control,GCTTTGCTACGATCTACATT,C. intestinalis
|
| 203 |
+
cut,S. pyogenes,Bleris,"<a href=""/64955/"">64955</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25527740/"">25527740</a>",negative control,GTCAAGGCACTCTTGCCTA,synthetic
|
| 204 |
+
,S. pyogenes,Wolfe,"<a href=""/50927/"">50927</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",negative control,GTTCCGCGTTACATAACTTA,H. sapiens
|
| 205 |
+
cut,S. pyogenes,Winslow,"<a href=""/67594/"">67594</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26178787/"">26178787</a>",Neomycin,TCATGGCTGATGCAATGCGG,
|
| 206 |
+
cut,S. pyogenes,Zhang,"<a href=""/60227/"">60227</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25263330/"">25263330</a>",NeuN,TCCGGTTCAGGGACCCCGAC,M. musculus
|
| 207 |
+
activate,S. pyogenes,Sato,"<a href=""/64161/"">64161</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",Neurog2,GTCTCTATCACTGATAGGGA,H. sapiens
|
| 208 |
+
activate,S. pyogenes,Sato,"<a href=""/64160/"">64160</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",Neurog2,GTGAATGATGATAATACGAT,H. sapiens
|
| 209 |
+
methylation,S. pyogenes,Zoldoš,"<a href=""/71830/"">71830</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26969735/"">26969735</a>",Non-targeting,GTAGGCGCGCCGCTCTCTAC,H. sapiens
|
| 210 |
+
cut,S. pyogenes,Huangfu,"<a href=""/66989/"">66989</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26028531/"">26028531</a>",OCT4,CTCCCATGCATTCAAACTG,H. sapiens
|
| 211 |
+
activate,S. pyogenes,Otonkoski,"<a href=""/69537/"">69537</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26352799/"">26352799</a>",OCT4,"multiple, see article",H. sapiens
|
| 212 |
+
cut,N. meningitidis,Thomson,"<a href=""/47870/"">47870</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23940360/"">23940360</a>",OCT4,GTTGTAGCTCCCTTTCTCATTTCG,H. sapiens
|
| 213 |
+
interfere,S. pyogenes,Wolfe,"<a href=""/50922/"">50922</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Oct4A (POU5F1),GGGGCGCCAGTTGTGTCTCC,H. sapiens
|
| 214 |
+
interfere,S. pyogenes,Wolfe,"<a href=""/50921/"">50921</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Oct4A (POU5F1),GTGGGACTGGGGAGGGAGAG,H. sapiens
|
| 215 |
+
cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",P450,CATATAGTTGGGTCATGGCA,O. sativa
|
| 216 |
+
cut,S. pyogenes,Sherwood,"<a href=""/71483/"">71483</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26527385/"">26527385</a>",Pal1,GCTCTGTGACTAGTCACAGAG,synthetic
|
| 217 |
+
cut,S. pyogenes,Sherwood,"<a href=""/71484/"">71484</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26527385/"">26527385</a>",Pal7,GGCTTAGTACTAGTACTAAGC,synthetic
|
| 218 |
+
cut,S. pyogenes,Zhang,"<a href=""/68466/"">68466</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26145478/"">26145478</a>",PAX6,GATACTGGAGAAGCAGGGGC,H. sapiens
|
| 219 |
+
cut,S. pyogenes,Zhang,"<a href=""/68465/"">68465</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26145478/"">26145478</a>",PAX6,GTAGATTTTGTATGCACTGC,H. sapiens
|
| 220 |
+
interfere,S. pyogenes,Lu,"<a href=""/68900/"">68900</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",PcfiA,AAACAAAACCTCATCAGGCA,
|
| 221 |
+
interfere,S. pyogenes,Lu,"<a href=""/68899/"">68899</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",PcfiA,GAAGCTCACTCCTTAGCACG,
|
| 222 |
+
interfere,S. pyogenes,Lu,"<a href=""/74068/"">74068</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pck,AGGGCGAGGCGCCGACCAAGAATA,C. glutamicum
|
| 223 |
+
interfere,S. pyogenes,Lu,"<a href=""/74069/"">74069</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pck,TCCAGTTCAGCAGTTCCTTATTCT,C. glutamicum
|
| 224 |
+
cut,S. pyogenes,Kamoun,"<a href=""/46966/"">46966</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23929340/"">23929340</a>",PDS,GCCGTTAATTTGAGAGTCCA,N. benthamiana
|
| 225 |
+
cut,S. pyogenes,Kamoun,"<a href=""/49772/"">49772</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24112467/"">24112467</a>",PDS1,"multiple, see article",N. benthamiana
|
| 226 |
+
cut,S. pyogenes,Sheen,"<a href=""/52255/"">52255</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23929339/"">23929339</a>",PDS3,GGACTTTTGCCAGCCATGGT,A. thaliana
|
| 227 |
+
interfere,S. pyogenes,Lu,"<a href=""/74066/"">74066</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pgi,TGACCGATCATTACTCAAACTTCC,C. glutamicum
|
| 228 |
+
interfere,S. pyogenes,Lu,"<a href=""/74067/"">74067</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pgi,TTGCCTGGAAGTTTGAGTAATGAT,C. glutamicum
|
| 229 |
+
activate,S. pyogenes,Gersbach,"<a href=""/60719/"">60719</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25664691/"">25664691</a>",pGL3-Basic-8x-gRNA-eGFP reporter,AAAGGTCGAGAAACTGCAAA,synthetic
|
| 230 |
+
cut,S. pyogenes,Ward,"<a href=""/61252/"">61252</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25491644/"">25491644</a>",pha-1,ATGAATAACTTGATGAACAT,C. elegans
|
| 231 |
+
cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",PHYB,CACTAGGAGCAACACCCAA,A. thaliana
|
| 232 |
+
cut,S. pyogenes,Wolfe,"<a href=""/69240/"">69240</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",PLXNB2,GGGCCCAACCTAGGGCATGG,H. sapiens
|
| 233 |
+
cut,S. pyogenes,Patron,"<a href=""/68253/"">68253</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26616834/"">26616834</a>",PM19,GCTCTCCACTCTGGGCTCTT,H. vulgare
|
| 234 |
+
cut,S. pyogenes,Patron,"<a href=""/68254/"">68254</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26616834/"">26616834</a>",PM19,GGCTGGCGTTGGTCGTAACA,H. vulgare
|
| 235 |
+
cut,S. pyogenes,Shaw,"<a href=""/79004/"">79004</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26944679/"">26944679</a>",Prkaa1,TTATTGTCACAGGCATATGG,M. musculus
|
| 236 |
+
cut,S. pyogenes,Shaw,"<a href=""/79005/"">79005</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26944679/"">26944679</a>",Prkaa2,ACAGGCATATGGTTGTCCAT,M. musculus
|
| 237 |
+
cut,S. pyogenes,Schmitt-Ulms,"<a href=""/61857/"">61857</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25490046/"">25490046</a>",Prnp,GGTGGAACACCGGTGGAAGC,M. musculus
|
| 238 |
+
cut,S. pyogenes,Schmitt-Ulms,"<a href=""/61856/"">61856</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25490046/"">25490046</a>",Prnp,TTGGCCCCATCCACCGCCAT,M. musculus
|
| 239 |
+
cut,N. meningitidis,Church,"<a href=""/48651/"">48651</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer A,TACCATCTCAAGCTTGTTGA,Synthetic
|
| 240 |
+
cut,S. thermophilus,Church,"<a href=""/48653/"">48653</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer A,TACCATCTCAAGCTTGTTGA,Synthetic
|
| 241 |
+
cut,T. denticola,Church,"<a href=""/48655/"">48655</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer A,TACCATCTCAAGCTTGTTGA,Synthetic
|
| 242 |
+
cut,N. meningitidis,Church,"<a href=""/48652/"">48652</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer B,ACTTTAAAAGTATTCGCCAT,Synthetic
|
| 243 |
+
cut,S. thermophilus,Church,"<a href=""/48654/"">48654</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer B,ACTTTAAAAGTATTCGCCAT,Synthetic
|
| 244 |
+
cut,T. denticola,Church,"<a href=""/48656/"">48656</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer B,ACTTTAAAAGTATTCGCCAT,Synthetic
|
| 245 |
+
cut,S. pyogenes,Moffat,"<a href=""/74184/"">74184</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",PSMB2,ATGTTCTTGTCGCCTCCGAC,H. sapiens
|
| 246 |
+
cut,S. pyogenes,Moffat,"<a href=""/74185/"">74185</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",PSMB2,AATATTGTCCAGATGAAGGA,H. sapiens
|
| 247 |
+
cut,S. pyogenes,Moffat,"<a href=""/74180/"">74180</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",PSMD1,TGTGCGCTACGGAGCTGCAA,H. sapiens
|
| 248 |
+
cut,S. pyogenes,Moffat,"<a href=""/74181/"">74181</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",PSMD1,ACCAGAGCCACAATAAGCCA,H. sapiens
|
| 249 |
+
cut,S. pyogenes,Jacks,"<a href=""/59909/"">59909</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25119044/"">25119044</a>",Pten,AGATCGTTAGCAGAAACAAA,M. musculus
|
| 250 |
+
cut,S. pyogenes,Xue,"<a href=""/66588/"">66588</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",Pten,GCTGTAGTAATATCTGCTAT,M. musculus
|
| 251 |
+
cut,S. pyogenes,Xue,"<a href=""/66587/"">66587</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",Pten,GTTTCATAGCGGCCACGAAGT,M. musculus
|
| 252 |
+
interfere,S. pyogenes,Lu,"<a href=""/74070/"">74070</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pyk,AGATTGTATGTACCCTAGGCCCAG,C. glutamicum
|
| 253 |
+
interfere,S. pyogenes,Lu,"<a href=""/74072/"">74072</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pyk,ATTCCATCTGCACTAGCCACCGCT,C. glutamicum
|
| 254 |
+
tag,S. pyogenes,Mendenhall,"<a href=""/64057/"">64057</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",RAD21,CCAAGGTTCCATATTATATAAGG,H. sapiens
|
| 255 |
+
cut,S. pyogenes,Fire,"<a href=""/59927/"">59927</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",rde-1(D718),TGCCATTAACTATGTATGT,C. elegans
|
| 256 |
+
cut,S. pyogenes,Fire,"<a href=""/59928/"">59928</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",rde-1(D801),GATATTGTAGTCTATCGAGA,C. elegans
|
| 257 |
+
cut,S. pyogenes,Fire,"<a href=""/59929/"">59929</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",rde-1(H974),GATAAATGAGCATAATGAAC,C. elegans
|
| 258 |
+
interfere,S. pyogenes,Lu,"<a href=""/74075/"">74075</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",rfp,AAGTTCGTATGGAAGGTTCCGTTAA,
|
| 259 |
+
cut,S. pyogenes,Joung,"<a href=""/42246/"">42246</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",rgs4,GGAGAAGGTGAAGGACACTG,D. rerio
|
| 260 |
+
cut,S. pyogenes,Bornhauser,"<a href=""/75165/"">75165</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27194728/"">27194728</a>",RIP3,TACACGAGTGATGGTTCGGT,H. sapiens
|
| 261 |
+
cut,S. pyogenes,Bornhauser,"<a href=""/75163/"">75163</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27194728/"">27194728</a>",RIPK1,GCTCTGCTGGGAAGCGAATC,H. sapiens
|
| 262 |
+
cut,S. pyogenes,Joung,"<a href=""/47509/"">47509</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",RNF2,GTCATCTTAGTCATTACCTG,H. sapiens
|
| 263 |
+
cut,S. pyogenes,Fire,"<a href=""/59930/"">59930</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",rol-6(su1006),GTGAGACGTCAACAATATGG,C. elegans
|
| 264 |
+
cut,S. pyogenes,Kuhn,"<a href=""/64216/"">64216</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25803306/"">25803306</a>",Rosa26,ACTCCAGTCTTTCTAGAAGA,M.musculus
|
| 265 |
+
cut,S. pyogenes,Marraffini,"<a href=""/44505/"">44505</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360965/"">23360965</a>",rpsL,AAAAAACCGAACTCCGCGCTGCGTAAAGTA,E. coli
|
| 266 |
+
interfere,S. pyogenes,Voigt,"<a href=""/62285/"">62285</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25422271/"">25422271</a>",scramble,AACCCCTGATTGTATCCGCA,synthetic
|
| 267 |
+
cut,S. pyogenes,Chen,"<a href=""/47930/"">47930</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23918387/"">23918387</a>",slc24a5 (golden),GGTCTCTCGCAGGATGTTGC,D. rerio
|
| 268 |
+
cut,S. pyogenes,Hanna,"<a href=""/59725/"">59725</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25543152/"">25543152</a>",SOX17,GCCCAGCCCGGCCATCACCG,H. sapiens
|
| 269 |
+
activate,S. pyogenes,Wolfe,"<a href=""/50925/"">50925</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Sox17,GCTCCGGCTAGTTTTCCCGG,H. sapiens
|
| 270 |
+
activate,S. pyogenes,Wolfe,"<a href=""/50924/"">50924</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Sox17,GGAGGGGCAAGGGGCGGGCG,H. sapiens
|
| 271 |
+
activate,S. pyogenes,Wolfe,"<a href=""/50926/"">50926</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Sox17,GGGCAAGTACGTCGATTCCA,H. sapiens
|
| 272 |
+
activate,S. pyogenes,Wolfe,"<a href=""/50923/"">50923</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Sox17,GGGCGTGGGCCTAACGACGC,H. sapiens
|
| 273 |
+
cut,S. pyogenes,Fire,"<a href=""/59935/"">59935</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",sqt-1,GGAAGGACATAGTTGTCAT,C. elegans
|
| 274 |
+
cut,S. pyogenes,Fire,"<a href=""/60212/"">60212</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",sqt-1,TGTGGAGTTGGGGTAGCGT,C. elegans
|
| 275 |
+
activate/interfere,S. pyogenes,Qi,"<a href=""/62338/"">62338</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",sv40 promoter,CATACTTCTGCCTGCTGGGGAGCCTG,synthetic
|
| 276 |
+
scaffold,S. pyogenes,Qi & Lim,"<a href=""/62340/"">62340</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",SV40.NT1,GAATAGCTCAGAGGCCGAGG,H. sapiens
|
| 277 |
+
cut,S. pyogenes,Seydoux,"<a href=""/66100/"">66100</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",swan-2,ACAAATTGATATCCAATCA,C. elegans
|
| 278 |
+
cut,S. pyogenes,Seydoux,"<a href=""/66099/"">66099</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",swan-2,TGAAGAAAGTTATACTCGA,C. elegans
|
| 279 |
+
cut,S. pyogenes,Hanna,"<a href=""/59726/"">59726</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25543152/"">25543152</a>",T (BRACHYURY),CACGCGCAGTTCGCGCTCTG,H. sapiens
|
| 280 |
+
cut,S. pyogenes,"Guigo, Johnson","<a href=""/72627/"">72627</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",TCRC,gRNA1:GGGGAGCGGGAAAGCGGTCG; gRNA2:AACTGACCTTCAGGCCCGTA,H. sapiens
|
| 281 |
+
cut,N. meningitidis,Thomson,"<a href=""/47869/"">47869</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23940360/"">23940360</a>",tdtomato,CGAAATGAGAAAGGGAGCTACAAC,Synthetic
|
| 282 |
+
scaffold,S. pyogenes,Qi & Lim,"<a href=""/62320/"">62320</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",TEF,TTGATATTTAAGTTAATAAA,S. cerevisiae
|
| 283 |
+
interfere,S. pyogenes,Weissman,"<a href=""/46922/"">46922</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",TEF1,TTGATATTTAAGTTAATAAA,S. cerevisiae
|
| 284 |
+
visualize,S. pyogenes,Qi,"<a href=""/51024/"">51024</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24360272/"">24360272</a>",telomeres,GTTAGGGTTAGGGTTAGGGTTA,H. sapiens
|
| 285 |
+
scaffold,S. pyogenes,Qi & Lim,"<a href=""/62313/"">62313</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",TET,ACTTTTCTCTATCACTGATA,S. cerevisiae
|
| 286 |
+
interfere,S. pyogenes,Weissman,"<a href=""/46923/"">46923</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",TET promoter,TATCAGTGATAGAGAAAAGT,
|
| 287 |
+
activate/interfere,S. pyogenes,Qi,"<a href=""/62327/"">62327</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",Tet3G,GTACGTTCTCTATCACTGATA,synthetic
|
| 288 |
+
cut,S. pyogenes,Shaw,"<a href=""/79006/"">79006</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26944679/"">26944679</a>",Tfeb,AGCACTGTTGCCGGCCGAGG,M. musculus
|
| 289 |
+
RNA targeting,S. pyogenes,Yeo,"<a href=""/74709/"">74709</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26997482/"">26997482</a>",TFRC,GCACUGACCAGAUAAGAAUG,H. sapiens
|
| 290 |
+
cut,S. pyogenes,"Guigo, Johnson","<a href=""/72626/"">72626</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",TFRC,"gRNA1:GCAGCCTCAGAAATACAAAA, gRNA2CGGGATATCGGGTGGCGGCT",H. sapiens
|
| 291 |
+
cut,S. pyogenes,Wolfe,"<a href=""/69239/"">69239</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",TGM2,CCTGGACCCAACGCCCCAGG,H. sapiens
|
| 292 |
+
cut,S. pyogenes,Sibley,"<a href=""/59855/"">59855</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25480939/"">25480939</a>",TGME49_290860,ATTGGTCTGACAGCGATGTG,T. gondii
|
| 293 |
+
cut,S. pyogenes,Joung,"<a href=""/42247/"">42247</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",th,GGATGCGCGTAAGGAGCGCG,D. rerio
|
| 294 |
+
cut,S. pyogenes,Du,"<a href=""/65565/"">65565</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25849248/"">25849248</a>",th,GGGTTGTTACACTCATAAAG,D. rerio
|
| 295 |
+
cut,S. pyogenes,Joung,"<a href=""/42248/"">42248</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",tia1l,GGTATGTCGGGAACCTCTCC,D. rerio
|
| 296 |
+
cut,S. pyogenes,Joung,"<a href=""/42249/"">42249</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",tph1a,GGGAAAACACAACCGCAGCC,D. rerio
|
| 297 |
+
scaffold,S. pyogenes,Qi & Lim,"<a href=""/62325/"">62325</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",TRE3G,GTACGTTCTCTATCACTGATA,H. sapiens
|
| 298 |
+
cut,S. pyogenes,Jin,"<a href=""/64331/"">64331</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25281382/"">25281382</a>",trp1,GTCAATTGTTCTCTTTCTAT,S. cerevisiae
|
| 299 |
+
cut,S. pyogenes,Jacks,"<a href=""/59910/"">59910</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25119044/"">25119044</a>",Trp53,CCTCGAGCTCCCTCTGAGCC,M. musculus
|
| 300 |
+
cut,S. pyogenes,Wolfe,"<a href=""/69234/"">69234</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",TS2,GACCCCCTCCACCCCGCCTC,H. sapiens
|
| 301 |
+
cut,S. pyogenes,Wolfe,"<a href=""/69235/"">69235</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",TS3,GGTGAGTGAGTGTGTGCGTG,H. sapiens
|
| 302 |
+
cut,S. pyogenes,Wolfe,"<a href=""/69236/"">69236</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",TS4,GAGTCCGAGCAGAAGAAGAA,H. sapiens
|
| 303 |
+
cut,S. pyogenes,Goldstein,"<a href=""/47550/"">47550</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23995389/"">23995389</a>",ttTi5605 Mos1 insertion site,ATATCAGTCTGTTTCGTAA,C. elegans
|
| 304 |
+
cut,S. pyogenes,Chen,"<a href=""/46761/"">46761</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23918387/"">23918387</a>",tyr,CCCCAGAAGTCCTCCAGTCC,D. rerio
|
| 305 |
+
cut,S. pyogenes,Chen,"<a href=""/64250/"">64250</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25855067/"">25855067</a>",tyr,GGACTGGAGGACTTCTGGGG,D. rerio
|
| 306 |
+
cut,S. pyogenes,Mashimo,"<a href=""/60966/"">60966</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Tyr (albino),TTTCCAGGATTATGTAATAG,R. norvegicus
|
| 307 |
+
cut,S. pyogenes,Mashimo,"<a href=""/60967/"">60967</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Tyr (wild type),TTTCCAGGATTACGTAATAG,R. norvegicus
|
| 308 |
+
cut,S. pyogenes,"Guigo, Johnson","<a href=""/72628/"">72628</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",UCA1,gRNA1:GGTTTCCTTTTAGATGACGG; gRNA2:TCTGAAAAGAGAGTCAGCGA,H. sapiens
|
| 309 |
+
cut,S. pyogenes,Fire,"<a href=""/59932/"">59932</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",unc-109(n499),GGAACTCGTGTCAAAACAAC,C. elegans
|
| 310 |
+
cut,S. pyogenes,Calarco,"<a href=""/46169/"">46169</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23817069/"">23817069</a>",unc-119,GAATTTTCTGAAATTAAAGA,C. elegans
|
| 311 |
+
cut,S. pyogenes,Mello,"<a href=""/58202/"">58202</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24879462/"">24879462</a>",unc-22,GAACCCGTTGCCGAATACAC,C. elegans
|
| 312 |
+
cut,S. pyogenes,Fire,"<a href=""/59931/"">59931</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",unc-58(e665),TCCACGCACATGGTCACTA,C. elegans
|
| 313 |
+
cut,S. pyogenes,Sibley,"<a href=""/54467/"">54467</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24825012/"">24825012</a>",UPRT,GGCGTCTCGATTGTGAGAGC,Toxoplasma gondii
|
| 314 |
+
cut,S. pyogenes,Jin,"<a href=""/64330/"">64330</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25281382/"">25281382</a>",ura3,GAGTAAAAAATTGTACTTGG,S. cerevisiae
|
| 315 |
+
cut,S. pyogenes,Ye,"<a href=""/61812/"">61812</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24424410/"">24424410</a>",USP13,GCCGCCCGGCATGCCGAACA,H. sapiens
|
| 316 |
+
cut,S. pyogenes,Joung,"<a href=""/47506/"">47506</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",VEGF,GACCCCCTCCACCCCGCCTC,H. sapiens
|
| 317 |
+
cut,S. pyogenes,Joung,"<a href=""/47505/"">47505</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",VEGF,GGGTGGGGGGAGTTTGCTCC,H. sapiens
|
| 318 |
+
cut,S. pyogenes,Joung,"<a href=""/47507/"">47507</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",VEGF,GGTGAGTGAGTGTGTGCGTG,H. sapiens
|
| 319 |
+
interfere,S. pyogenes,Voigt,"<a href=""/62286/"">62286</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25422271/"">25422271</a>",VR,TGCGCTCGGTCGTTCGGCTG,synthetic
|
| 320 |
+
nick,S. pyogenes,Yang,"<a href=""/73228/"">73228</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27213844/"">27213844</a>",xylR,CGAGTTAGACATAATAGTGA,C. beijerinckii
|
| 321 |
+
cut,S. pyogenes,Ward,"<a href=""/61250/"">61250</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25491644/"">25491644</a>",Y61A9LA.1,GGATGGATGTGTAGTCAATT,C. elegans
|
| 322 |
+
cut,S. pyogenes,Liu,"<a href=""/49331/"">49331</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24326186/"">24326186</a>",yellow,GGTTTTGGACACTGGAACCG,D. melanogaster
|
Biomanus_upload/biomni/know_how/sgRNA_design_guide.md
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# sgRNA Design Guide: Three-Tiered Approach
|
| 2 |
+
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
## Metadata
|
| 6 |
+
|
| 7 |
+
**Short Description**: Comprehensive guide for finding or designing sgRNAs using validated sequences, CRISPick datasets, or de novo design tools.
|
| 8 |
+
|
| 9 |
+
**Authors**: Biomni Team
|
| 10 |
+
|
| 11 |
+
**Version**: 1.0
|
| 12 |
+
|
| 13 |
+
**Last Updated**: November 2025
|
| 14 |
+
|
| 15 |
+
**License**: CC BY 4.0
|
| 16 |
+
|
| 17 |
+
**Commercial Use**: ✅ Allowed
|
| 18 |
+
|
| 19 |
+
## Citations and Acknowledgments
|
| 20 |
+
|
| 21 |
+
### If you use validated sgRNAs from our database (Option 1):
|
| 22 |
+
- **Database Source**: Addgene (https://www.addgene.org)
|
| 23 |
+
- **Citation**: Always cite the original publication associated with each sgRNA using the PubMed ID provided in the database
|
| 24 |
+
- **Acknowledgment**: "Validated sgRNA sequences obtained from Addgene (https://www.addgene.org/crispr/reference/grna-sequence/)"
|
| 25 |
+
|
| 26 |
+
### If you use CRISPick designs (Option 2):
|
| 27 |
+
- **Acknowledgment Statement**: "Guide designs provided by the CRISPick web tool of the GPP at the Broad Institute"
|
| 28 |
+
- **Citation for Cas9 designs (SpCas9, SaCas9)**: Sanson KR, et al. Optimized libraries for CRISPR-Cas9 genetic screens with multiple modalities. Nat Commun. 2018;9(1):5416. PMID: 30575746
|
| 29 |
+
- **Citation for Cas12a designs (AsCas12a, enAsCas12a)**: DeWeirdt PC, et al. Optimization of AsCas12a for combinatorial genetic screens in human cells. Nat Biotechnol. 2021;39(1):94-104. PMID: 32661438
|
| 30 |
+
- Note: This paper describes enAsCas12a optimization; specify which variant you used in your methods
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## Overview
|
| 35 |
+
|
| 36 |
+
This guide provides a three-tiered approach to sgRNA design, prioritizing validated sequences before moving to computational predictions. Always start with Option 1 and proceed to subsequent options only if needed.
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
## Option 1: Search Validated sgRNA Sequences (Recommended First)
|
| 41 |
+
|
| 42 |
+
### 1.1 Search for Validated sgRNAs
|
| 43 |
+
|
| 44 |
+
**IMPORTANT**: You MUST complete BOTH Method 1 AND Method 2 before proceeding to Option 2. Do not skip Method 2 even if Method 1 finds no results.
|
| 45 |
+
|
| 46 |
+
#### Method 1: Search Our Database (Fastest)
|
| 47 |
+
|
| 48 |
+
We maintain a curated database of 300+ validated sgRNA sequences from Addgene with experimental evidence.
|
| 49 |
+
|
| 50 |
+
**Location**: `biomni/know_how/resource/addgene_grna_sequences.csv`
|
| 51 |
+
|
| 52 |
+
**Search the database**:
|
| 53 |
+
```python
|
| 54 |
+
import pandas as pd
|
| 55 |
+
|
| 56 |
+
# Load the database
|
| 57 |
+
df = pd.read_csv('addgene_grna_sequences.csv')
|
| 58 |
+
|
| 59 |
+
# Search for your gene
|
| 60 |
+
gene_name = "TP53"
|
| 61 |
+
results = df[df['Target_Gene'].str.upper() == gene_name.upper()]
|
| 62 |
+
|
| 63 |
+
# Filter by species and application
|
| 64 |
+
results_filtered = results[
|
| 65 |
+
(results['Target_Species'] == 'H. sapiens') &
|
| 66 |
+
(results['Application'] == 'cut') # or 'activate', 'RNA targeting'
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
# Display results with references
|
| 70 |
+
print(results_filtered[['Target_Gene', 'Target_Sequence',
|
| 71 |
+
'Plasmid_ID', 'PubMed_ID', 'Depositor']])
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
**Database columns**:
|
| 75 |
+
- `Target_Gene`: Gene symbol
|
| 76 |
+
- `Target_Species`: Organism (H. sapiens, M. musculus, etc.)
|
| 77 |
+
- `Target_Sequence`: 20bp sgRNA sequence (5' to 3')
|
| 78 |
+
- `Application`: cut (knockout), activate (CRISPRa), RNA targeting (CRISPRi)
|
| 79 |
+
- `Cas9_Species`: S. pyogenes, S. aureus, etc.
|
| 80 |
+
- `Plasmid_ID`: Addgene plasmid number
|
| 81 |
+
- `Plasmid_URL`: Direct link to plasmid page
|
| 82 |
+
- `PubMed_ID`: **Publication reference** (cite this in your work)
|
| 83 |
+
- `PubMed_URL`: Direct link to paper
|
| 84 |
+
- `Depositor`: Research lab that contributed the sequence
|
| 85 |
+
|
| 86 |
+
#### Method 2: Advanced Web Search (REQUIRED - Do Not Skip)
|
| 87 |
+
|
| 88 |
+
**CRITICAL**: Even if Method 1 found no results, you MUST perform this literature search before moving to Option 2. Many validated sgRNAs are published in literature but not in the Addgene database.
|
| 89 |
+
|
| 90 |
+
Use `advanced_web_search_claude` from `biomni.tool.literature` to find validated sgRNAs from literature and databases:
|
| 91 |
+
|
| 92 |
+
```python
|
| 93 |
+
from biomni.tool.literature import advanced_web_search_claude
|
| 94 |
+
|
| 95 |
+
# Example usage
|
| 96 |
+
results = advanced_web_search_claude("sgRNA TP53 validated H. sapiens experimental")
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
**Search queries to try (use multiple):**
|
| 100 |
+
```
|
| 101 |
+
"sgRNA" OR "guide RNA" "[GENE_NAME]" validated experimental
|
| 102 |
+
"CRISPR knockout" "[GENE_NAME]" sgRNA sequence validated
|
| 103 |
+
"[GENE_NAME]" sgRNA "cutting efficiency" OR "on-target"
|
| 104 |
+
"[GENE_NAME]" "guide sequence" CRISPR validated
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
**Example for TP53:**
|
| 108 |
+
```
|
| 109 |
+
"sgRNA" "TP53" validated "H. sapiens" experimental
|
| 110 |
+
"CRISPR knockout" "TP53" guide sequence validated
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
**What to search for in results:**
|
| 114 |
+
- Published papers with validated sgRNA sequences
|
| 115 |
+
- Supplementary materials containing sgRNA sequences
|
| 116 |
+
- Other CRISPR databases (e.g., GenScript, Horizon Discovery)
|
| 117 |
+
- Laboratory protocols with specific sgRNA sequences
|
| 118 |
+
|
| 119 |
+
**IMPORTANT**: Spend adequate time searching literature. Look through at least the first 10-15 search results and check supplementary materials of relevant papers.
|
| 120 |
+
|
| 121 |
+
#### What to Do with Results:
|
| 122 |
+
|
| 123 |
+
✅ **If you find matching sgRNAs (from either method):**
|
| 124 |
+
1. **Record the sgRNA sequence** (20bp target sequence)
|
| 125 |
+
2. **Note the reference**:
|
| 126 |
+
- From database: Use `PubMed_ID` to cite the original paper
|
| 127 |
+
- From web search: Record the publication DOI/PubMed ID
|
| 128 |
+
3. **Record validation details**: Cell line, cutting efficiency, any reported off-targets
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
**Example result format:**
|
| 132 |
+
```
|
| 133 |
+
Gene: TP53
|
| 134 |
+
sgRNA sequence: GAGGTTGTGAGGCGCTGCCC
|
| 135 |
+
Species: H. sapiens (human)
|
| 136 |
+
Application: Knockout (cut)
|
| 137 |
+
Reference: PubMed ID 24336569 (Ran et al., 2013)
|
| 138 |
+
Validation: Tested in HEK293T cells, 85% cutting efficiency
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
❌ **If no matches found in BOTH Method 1 AND Method 2:**
|
| 142 |
+
Only then proceed to **Option 2: Download CRISPick Dataset**
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
## Option 2: Download Pre-Computed sgRNAs from CRISPick
|
| 147 |
+
|
| 148 |
+
### When to Use This Option?
|
| 149 |
+
- No validated sgRNAs found in Addgene
|
| 150 |
+
- Need comprehensive coverage of a gene
|
| 151 |
+
- Want multiple sgRNA options with predicted scores
|
| 152 |
+
- Working with genome-scale screening
|
| 153 |
+
|
| 154 |
+
### 2.1 Overview of CRISPick
|
| 155 |
+
|
| 156 |
+
**CRISPick** (from Broad Institute GPP) provides pre-computed sgRNA designs for entire genomes with 238 available datasets covering:
|
| 157 |
+
- Human (GRCh38, GRCh37)
|
| 158 |
+
- Mouse (GRCm38, GRCm39)
|
| 159 |
+
- Dog, Cow, Monkey, and other model organisms
|
| 160 |
+
- Multiple Cas enzymes (SpCas9, SaCas9, AsCas12a, enAsCas12a)
|
| 161 |
+
- **IMPORTANT**: AsCas12a and enAsCas12a are DIFFERENT enzymes
|
| 162 |
+
- **AsCas12a**: Wild-type Acidaminococcus sp. Cas12a
|
| 163 |
+
- **enAsCas12a**: Enhanced variant with improved activity
|
| 164 |
+
- **Critical**: sgRNAs designed for enAsCas12a may NOT work with wild-type AsCas12a
|
| 165 |
+
- Always match the dataset to your specific Cas12a variant
|
| 166 |
+
- Different applications (knockout, activation, inhibition)
|
| 167 |
+
|
| 168 |
+
### 2.2 Find the Download Link
|
| 169 |
+
|
| 170 |
+
**All 238 download links are available in**: `biomni/know_how/resource/CRISPick_download_links.txt`
|
| 171 |
+
|
| 172 |
+
#### Step 1: Understand File Naming Convention
|
| 173 |
+
|
| 174 |
+
Files are named: `sgRNA_design_{TAXID}_{GENOME}_{CAS}_{APPLICATION}_{ALGORITHM}_{SOURCE}_{DATE}.txt.gz`
|
| 175 |
+
|
| 176 |
+
**Common datasets:**
|
| 177 |
+
|
| 178 |
+
| Organism | Genome | Cas9 | Application | Search Pattern |
|
| 179 |
+
|-|--||-|-|
|
| 180 |
+
| Human | GRCh38 | SpCas9 | Knockout | `9606_GRCh38_SpyoCas9_CRISPRko` |
|
| 181 |
+
| Human | GRCh38 | SpCas9 | Activation | `9606_GRCh38_SpyoCas9_CRISPRa` |
|
| 182 |
+
| Human | GRCh38 | SaCas9 | Knockout | `9606_GRCh38_SaurCas9_CRISPRko` |
|
| 183 |
+
| Mouse | GRCm38 | SpCas9 | Knockout | `10090_GRCm38_SpyoCas9_CRISPRko` |
|
| 184 |
+
| Mouse | GRCm38 | SpCas9 | Activation | `10090_GRCm38_SpyoCas9_CRISPRa` |
|
| 185 |
+
|
| 186 |
+
**Key components:**
|
| 187 |
+
- **TAXID**: `9606` (Human), `10090` (Mouse), `9615` (Dog), `9913` (Cow)
|
| 188 |
+
- **CAS**:
|
| 189 |
+
- `SpyoCas9` (SpCas9, NGG PAM)
|
| 190 |
+
- `SaurCas9` (SaCas9, NNGRRT PAM)
|
| 191 |
+
- `AsCas12a` (Wild-type Cas12a, TTTV PAM)
|
| 192 |
+
- `enAsCas12a` (Enhanced Cas12a, TTTV PAM)
|
| 193 |
+
- **WARNING**: Do NOT use AsCas12a datasets if you have enAsCas12a, and vice versa
|
| 194 |
+
- **APPLICATION**: `CRISPRko` (knockout), `CRISPRa` (activation), `CRISPRi` (inhibition)
|
| 195 |
+
|
| 196 |
+
#### Step 2: Find Your Download URL
|
| 197 |
+
|
| 198 |
+
```bash
|
| 199 |
+
# Search the download links file
|
| 200 |
+
grep "9606_GRCh38_SpyoCas9_CRISPRko" biomni/know_how/resource/CRISPick_download_links.txt
|
| 201 |
+
|
| 202 |
+
# Or for mouse
|
| 203 |
+
grep "10090_GRCm38_SpyoCas9_CRISPRko" biomni/know_how/resource/CRISPick_download_links.txt
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
#### Step 3: Download and Extract
|
| 207 |
+
|
| 208 |
+
```bash
|
| 209 |
+
# Copy the URL from download_links.txt, then:
|
| 210 |
+
wget [PASTE_URL_HERE]
|
| 211 |
+
|
| 212 |
+
# Extract the file
|
| 213 |
+
gunzip sgRNA_design_*.txt.gz
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
**File sizes**: Knockout (300-700 MB), Activation (50-100 MB), Summary files (1-3 MB)
|
| 217 |
+
|
| 218 |
+
### 2.4 Understanding the File Format
|
| 219 |
+
|
| 220 |
+
The `.txt` file is tab-delimited. Column names differ between knockout and activation/inhibition datasets.
|
| 221 |
+
|
| 222 |
+
**Essential Columns (All files):**
|
| 223 |
+
- **sgRNA Sequence**: The 20bp guide RNA sequence (5' to 3')
|
| 224 |
+
- **Target Gene Symbol**: Gene name (e.g., "TP53", "BRCA1")
|
| 225 |
+
- **Combined Rank**: Overall ranking (lower = better) - **Use this by default**
|
| 226 |
+
- **On-Target Rank**: Ranking by efficiency only (lower = better)
|
| 227 |
+
- **Off-Target Rank**: Ranking by specificity only (lower = better)
|
| 228 |
+
- **PAM Sequence**: The PAM sequence (e.g., "AGG", "TGG")
|
| 229 |
+
|
| 230 |
+
**Knockout-specific columns:**
|
| 231 |
+
- **sgRNA Cut Position (1-based)**: Genomic coordinate of cut site
|
| 232 |
+
- **Exon Number**: Which exon is targeted
|
| 233 |
+
- **Target Cut %**: Percentage of protein affected
|
| 234 |
+
|
| 235 |
+
**Activation/Inhibition-specific columns:**
|
| 236 |
+
- **sgRNA 'Cut' Position**: Position relative to gene
|
| 237 |
+
- **sgRNA 'Cut' Site TSS Offset**: Distance from transcription start site (bp)
|
| 238 |
+
- **DHS Score**: DNase hypersensitivity score (for CRISPRa)
|
| 239 |
+
|
| 240 |
+
### 2.5 Extract and Select sgRNAs
|
| 241 |
+
|
| 242 |
+
#### Step 1: Load Data and Filter for Your Gene
|
| 243 |
+
|
| 244 |
+
```python
|
| 245 |
+
import pandas as pd
|
| 246 |
+
|
| 247 |
+
# Load the dataset
|
| 248 |
+
df = pd.read_csv('sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_*.txt',
|
| 249 |
+
sep='\t', low_memory=False)
|
| 250 |
+
|
| 251 |
+
# Filter for your gene
|
| 252 |
+
gene_name = "TP53"
|
| 253 |
+
gene_sgrnas = df[df['Target Gene Symbol'] == gene_name].copy()
|
| 254 |
+
|
| 255 |
+
print(f"Found {len(gene_sgrnas)} sgRNAs for {gene_name}")
|
| 256 |
+
```
|
| 257 |
+
|
| 258 |
+
#### Step 2: Select Top sgRNAs
|
| 259 |
+
|
| 260 |
+
**Default: Use Combined Rank (balances efficiency and specificity)**
|
| 261 |
+
```python
|
| 262 |
+
# Sort by Combined Rank (lower is better)
|
| 263 |
+
top_sgrnas = gene_sgrnas.nsmallest(10, 'Combined Rank')
|
| 264 |
+
|
| 265 |
+
print(top_sgrnas[['sgRNA Sequence', 'Combined Rank',
|
| 266 |
+
'Exon Number', 'sgRNA Cut Position (1-based)']])
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
**Option A: Prioritize On-Target Efficiency**
|
| 270 |
+
```python
|
| 271 |
+
# Sort by On-Target Rank (for maximum cutting efficiency)
|
| 272 |
+
efficient_sgrnas = gene_sgrnas.nsmallest(10, 'On-Target Rank')
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
**Option B: Prioritize Off-Target Specificity**
|
| 276 |
+
```python
|
| 277 |
+
# Sort by Off-Target Rank (for maximum specificity)
|
| 278 |
+
specific_sgrnas = gene_sgrnas.nsmallest(10, 'Off-Target Rank')
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
#### Step 3: Filter by Custom Criteria (Optional)
|
| 282 |
+
|
| 283 |
+
**Filter by Exon Number:**
|
| 284 |
+
```python
|
| 285 |
+
# Target specific exon (e.g., exon 5)
|
| 286 |
+
exon5_sgrnas = gene_sgrnas[gene_sgrnas['Exon Number'] == 5]
|
| 287 |
+
top_exon5 = exon5_sgrnas.nsmallest(5, 'Combined Rank')
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
**Filter by Genomic Position:**
|
| 291 |
+
```python
|
| 292 |
+
# Target specific genomic range
|
| 293 |
+
position_filtered = gene_sgrnas[
|
| 294 |
+
(gene_sgrnas['sgRNA Cut Position (1-based)'] >= 7572000) &
|
| 295 |
+
(gene_sgrnas['sgRNA Cut Position (1-based)'] <= 7575000)
|
| 296 |
+
]
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
**Target Early Exons for Knockout:**
|
| 300 |
+
```python
|
| 301 |
+
# Get sgRNAs from first 3 exons
|
| 302 |
+
early_exons = gene_sgrnas[gene_sgrnas['Exon Number'] <= 3]
|
| 303 |
+
top_early = early_exons.nsmallest(10, 'Combined Rank')
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
**Filter by Target Cut Percentage:**
|
| 307 |
+
```python
|
| 308 |
+
# Target sgRNAs that affect significant portion of protein
|
| 309 |
+
high_impact = gene_sgrnas[gene_sgrnas['Target Cut %'] <= 50] # Cut in first 50%
|
| 310 |
+
top_high_impact = high_impact.nsmallest(10, 'Combined Rank')
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
#### Step 4: Select Multiple sgRNAs for Validation
|
| 314 |
+
|
| 315 |
+
```python
|
| 316 |
+
# Get top 4 sgRNAs from different exons for redundancy
|
| 317 |
+
final_selection = gene_sgrnas.sort_values('Combined Rank').groupby('Exon Number').head(1).head(4)
|
| 318 |
+
|
| 319 |
+
# Save results
|
| 320 |
+
final_selection.to_csv(f'{gene_name}_selected_sgRNAs.csv', index=False)
|
| 321 |
+
|
| 322 |
+
print("\nSelected sgRNAs:")
|
| 323 |
+
print(final_selection[['sgRNA Sequence', 'Exon Number', 'Combined Rank']])
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
### 2.6 What to Do with Results
|
| 327 |
+
|
| 328 |
+
✅ **Once you have selected sgRNAs:**
|
| 329 |
+
1. Choose **3-4 sgRNAs** (use Combined_Rank by default)
|
| 330 |
+
|
| 331 |
+
❌ **If dataset doesn't cover your gene or organism:**
|
| 332 |
+
Proceed to **Option 3: De Novo sgRNA Design**
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
## Option 3: General sgRNA Design Guidelines (Last Resort)
|
| 337 |
+
|
| 338 |
+
### When to Use This Option?
|
| 339 |
+
- Gene not covered in CRISPick datasets
|
| 340 |
+
- Non-model organism
|
| 341 |
+
- Custom design requirements
|
| 342 |
+
|
| 343 |
+
### General Design Rules
|
| 344 |
+
|
| 345 |
+
#### Essential Requirements:
|
| 346 |
+
1. **Length**:
|
| 347 |
+
- 20 bp for SpCas9 and SaCas9
|
| 348 |
+
- 23-25 bp for Cas12a variants (AsCas12a, enAsCas12a)
|
| 349 |
+
2. **PAM sequence**:
|
| 350 |
+
- **SpCas9**: Requires **NGG** immediately after target (3' end)
|
| 351 |
+
- **SaCas9**: Requires **NNGRRT** immediately after target (3' end)
|
| 352 |
+
- **AsCas12a** (wild-type): Requires **TTTV** before target (5' end)
|
| 353 |
+
- **enAsCas12a** (enhanced): Requires **TTTV** before target (5' end)
|
| 354 |
+
- **CRITICAL**: Guides designed for enAsCas12a may not work with wild-type AsCas12a due to different activity profiles
|
| 355 |
+
3. **GC content**: 40-60% is optimal
|
| 356 |
+
4. **Avoid**:
|
| 357 |
+
- TTTT sequences (terminates transcription)
|
| 358 |
+
- Long runs of same nucleotide (>4 repeats)
|
| 359 |
+
|
| 360 |
+
#### Target Location:
|
| 361 |
+
- **For knockout**: Target early exons (first 50% of gene)
|
| 362 |
+
- **For activation**: Target -200 to +1 bp from transcription start site (TSS)
|
| 363 |
+
- **For inhibition**: Target -50 to +300 bp from TSS
|
| 364 |
+
|
| 365 |
+
#### Best Practices:
|
| 366 |
+
- **Test 3-4 different sgRNAs** per target gene
|
| 367 |
+
- Select sgRNAs with high efficiency scores (>0.5) and minimal off-targets
|
| 368 |
+
- Validate experimentally with Sanger sequencing
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
## Quick Start Examples
|
| 373 |
+
|
| 374 |
+
### Example 1: Knockout TP53 in Human Cells
|
| 375 |
+
|
| 376 |
+
**Step 1**: Check Addgene
|
| 377 |
+
```python
|
| 378 |
+
df = pd.read_csv('addgene_grna_sequences.csv')
|
| 379 |
+
tp53_results = df[(df['Target_Gene'] == 'TP53') &
|
| 380 |
+
(df['Target_Species'] == 'H. sapiens') &
|
| 381 |
+
(df['Application'] == 'cut')]
|
| 382 |
+
# Result: Found 0 entries → Proceed to Option 2
|
| 383 |
+
```
|
| 384 |
+
|
| 385 |
+
**Step 2**: Download CRISPick dataset
|
| 386 |
+
```bash
|
| 387 |
+
# Download human GRCh38 SpCas9 knockout dataset
|
| 388 |
+
wget https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/\
|
| 389 |
+
sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241104.txt.gz
|
| 390 |
+
|
| 391 |
+
gunzip sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_*.txt.gz
|
| 392 |
+
```
|
| 393 |
+
|
| 394 |
+
**Step 3**: Extract TP53 sgRNAs
|
| 395 |
+
```python
|
| 396 |
+
df = pd.read_csv('sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_*.txt', sep='\t')
|
| 397 |
+
tp53 = df[df['Gene_Symbol'] == 'TP53']
|
| 398 |
+
top_sgrnas = tp53[
|
| 399 |
+
(tp53['sgRNA_score'] > 0.6) &
|
| 400 |
+
(tp53['Off_target_stringency'] > 0.5)
|
| 401 |
+
].sort_values('sgRNA_score', ascending=False).head(4)
|
| 402 |
+
|
| 403 |
+
print(top_sgrnas[['sgRNA_sequence', 'sgRNA_score', 'Exon_ID']])
|
| 404 |
+
```
|
| 405 |
+
|
| 406 |
+
### Example 2: Activate OCT4 in Human iPSCs
|
| 407 |
+
|
| 408 |
+
**Step 1**: Check Addgene
|
| 409 |
+
```python
|
| 410 |
+
oct4_results = df[(df['Target_Gene'] == 'OCT4') &
|
| 411 |
+
(df['Application'] == 'activate')]
|
| 412 |
+
# Found 1 validated sgRNA!
|
| 413 |
+
print(oct4_results['Target_Sequence'].values[0])
|
| 414 |
+
# Use this sequence ✅
|
| 415 |
+
```
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
**Remember**: Always start with validated sequences (Option 1), then move to pre-computed designs (Option 2), and only use de novo design (Option 3) when necessary. Testing 3-4 sgRNAs per gene is standard practice regardless of prediction scores.
|
Biomanus_upload/biomni/know_how/single_cell_annotation.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Single Cell RNA-seq Cell Type Annotation
|
| 2 |
+
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
## Metadata
|
| 6 |
+
|
| 7 |
+
**Short Description**: Best practices for annotating cell types in single-cell RNA-seq data using marker-based, automated, and reference-based approaches.
|
| 8 |
+
|
| 9 |
+
**Authors**: Distilled from "Single-cell best practices" by Luecken, M.D. et al.
|
| 10 |
+
|
| 11 |
+
**Affiliations**: Helmholtz Munich, Wellcome Sanger Institute, Harvard Medical School, and contributors
|
| 12 |
+
|
| 13 |
+
**Version**: 1.0
|
| 14 |
+
|
| 15 |
+
**Last Updated**: January 2025
|
| 16 |
+
|
| 17 |
+
**License**: CC BY 4.0
|
| 18 |
+
|
| 19 |
+
**Commercial Use**: ✅ Allowed
|
| 20 |
+
|
| 21 |
+
**Source**: https://www.sc-best-practices.org/cellular_structure/annotation.html
|
| 22 |
+
|
| 23 |
+
**Citation**: Luecken, M.D., Theis, F.J. et al. (2023). Current best practices in single-cell RNA-seq analysis: a tutorial. Molecular Systems Biology.
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## Overview
|
| 28 |
+
|
| 29 |
+
Cell type annotation is the process of assigning cell type labels to clusters or individual cells in single-cell RNA-seq data. This guide covers three main approaches and their practical implementation.
|
| 30 |
+
|
| 31 |
+
## Three Annotation Approaches
|
| 32 |
+
|
| 33 |
+
### 1. Manual Marker-Based Annotation
|
| 34 |
+
Identify cell types by examining expression of known marker genes in each cluster.
|
| 35 |
+
|
| 36 |
+
**Tools**: Scanpy, Seurat
|
| 37 |
+
**Best for**: Small datasets, novel cell types, high confidence needs
|
| 38 |
+
|
| 39 |
+
### 2. Automated Annotation
|
| 40 |
+
Use pre-trained classifiers to automatically assign cell type labels.
|
| 41 |
+
|
| 42 |
+
**Tools**: CellTypist, scAnnotate
|
| 43 |
+
**Best for**: Standard tissues, quick preliminary annotation, large datasets
|
| 44 |
+
|
| 45 |
+
### 3. Reference-Based Label Transfer
|
| 46 |
+
Transfer labels from annotated reference datasets to your query data.
|
| 47 |
+
|
| 48 |
+
**Tools**: scArches, scANVI, Azimuth, SingleR
|
| 49 |
+
**Best for**: Well-characterized tissues, integration with public data
|
| 50 |
+
|
| 51 |
+
## Recommended Workflow
|
| 52 |
+
|
| 53 |
+
### Step 1: Quality Control First
|
| 54 |
+
- **Remove low-quality cells before annotation**
|
| 55 |
+
- Filter doublets (expected doublet rate: 0.8% per 1000 cells)
|
| 56 |
+
- Check for ambient RNA contamination
|
| 57 |
+
- Verify cluster quality and resolution
|
| 58 |
+
|
| 59 |
+
### Step 2: Initial Marker-Based Assessment
|
| 60 |
+
|
| 61 |
+
```python
|
| 62 |
+
# Scanpy example
|
| 63 |
+
import scanpy as sc
|
| 64 |
+
|
| 65 |
+
# Calculate marker genes for clusters
|
| 66 |
+
sc.tl.rank_genes_groups(adata, 'leiden', method='wilcoxon')
|
| 67 |
+
|
| 68 |
+
# Visualize top markers
|
| 69 |
+
sc.pl.rank_genes_groups(adata, n_genes=25, sharey=False)
|
| 70 |
+
|
| 71 |
+
# Plot known markers
|
| 72 |
+
markers = {
|
| 73 |
+
'T cells': ['CD3D', 'CD3E', 'CD4', 'CD8A'],
|
| 74 |
+
'B cells': ['CD19', 'MS4A1', 'CD79A'],
|
| 75 |
+
'Monocytes': ['CD14', 'FCGR3A', 'LYZ'],
|
| 76 |
+
'NK cells': ['NCAM1', 'NKG7', 'GNLY']
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
sc.pl.dotplot(adata, markers, groupby='leiden')
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
### Step 3: Use Automated Tools for Validation
|
| 83 |
+
|
| 84 |
+
```python
|
| 85 |
+
# CellTypist example (fast, accurate for immune cells)
|
| 86 |
+
import celltypist
|
| 87 |
+
from celltypist import models
|
| 88 |
+
|
| 89 |
+
# Download immune cell model
|
| 90 |
+
model = models.Model.load(model='Immune_All_Low.pkl')
|
| 91 |
+
|
| 92 |
+
# Predict cell types
|
| 93 |
+
predictions = celltypist.annotate(adata, model=model, majority_voting=True)
|
| 94 |
+
adata = predictions.to_adata()
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
### Step 4: Reference-Based Refinement
|
| 98 |
+
|
| 99 |
+
```python
|
| 100 |
+
# scArches example for label transfer
|
| 101 |
+
import scarches as sca
|
| 102 |
+
|
| 103 |
+
# Load pre-trained reference model
|
| 104 |
+
model = sca.models.SCANVI.load_query_data(
|
| 105 |
+
adata=adata, # Your query data
|
| 106 |
+
reference_model="path/to/reference_model"
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
# Transfer labels
|
| 110 |
+
model.train(max_epochs=100)
|
| 111 |
+
adata.obs['transferred_labels'] = model.predict()
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
## Best Practices
|
| 115 |
+
|
| 116 |
+
### Do's:
|
| 117 |
+
1. **Always combine multiple approaches** - Use marker-based validation even with automated tools
|
| 118 |
+
2. **Check cluster purity** - Ensure clusters represent single cell types
|
| 119 |
+
3. **Validate with multiple marker sets** - Don't rely on single markers
|
| 120 |
+
4. **Consider biological context** - Tissue type, disease state, developmental stage
|
| 121 |
+
5. **Document confidence levels** - Note uncertain annotations
|
| 122 |
+
6. **Use hierarchical annotation** - Broad categories first, then subtypes
|
| 123 |
+
|
| 124 |
+
### Don'ts:
|
| 125 |
+
1. **Don't over-cluster** - Too fine resolution creates artificial distinctions
|
| 126 |
+
2. **Don't ignore batch effects** - Correct before annotation
|
| 127 |
+
3. **Don't trust automation blindly** - Always validate predictions
|
| 128 |
+
4. **Don't mix cell states with cell types** - Activated vs. resting cells are states, not types
|
| 129 |
+
5. **Don't annotate low-quality cells** - Remove them first
|
| 130 |
+
|
| 131 |
+
## Common Pitfalls
|
| 132 |
+
|
| 133 |
+
### 1. Doublet Clusters
|
| 134 |
+
**Problem**: Clusters with markers from multiple cell types
|
| 135 |
+
**Solution**: Use doublet detection tools (Scrublet, DoubletFinder) before annotation
|
| 136 |
+
|
| 137 |
+
### 2. Ambient RNA
|
| 138 |
+
**Problem**: Background markers in all cells
|
| 139 |
+
**Solution**: Use SoupX or CellBender for decontamination
|
| 140 |
+
|
| 141 |
+
### 3. Over-interpretation of Small Clusters
|
| 142 |
+
**Problem**: Rare clusters may be technical artifacts
|
| 143 |
+
**Solution**: Require minimum cell count (e.g., >25 cells), validate with independent data
|
| 144 |
+
|
| 145 |
+
### 4. Reference Mismatch
|
| 146 |
+
**Problem**: Reference from different tissue/species/condition
|
| 147 |
+
**Solution**: Use tissue-matched references, check marker overlap before transfer
|
| 148 |
+
|
| 149 |
+
## Tool Selection Guide
|
| 150 |
+
|
| 151 |
+
| Scenario | Recommended Tool | Why |
|
| 152 |
+
|----------|------------------|-----|
|
| 153 |
+
| Immune cells (human) | CellTypist | Pre-trained on large immune atlases |
|
| 154 |
+
| Mouse tissues | scArches + Mouse Cell Atlas | Comprehensive mouse reference |
|
| 155 |
+
| Novel cell types | Manual + Scanpy/Seurat | Need domain expertise |
|
| 156 |
+
| Large datasets (>100k cells) | CellTypist | Fast, scalable |
|
| 157 |
+
| Cross-species | Manual markers | Limited reference transfer |
|
| 158 |
+
| Developmental data | scArches | Handles continuous states |
|
| 159 |
+
|
| 160 |
+
## Key Marker Genes by Cell Type
|
| 161 |
+
|
| 162 |
+
### Blood/Immune:
|
| 163 |
+
- **T cells**: CD3D, CD3E (all T cells); CD4, CD8A (subtypes)
|
| 164 |
+
- **B cells**: CD19, MS4A1 (CD20), CD79A
|
| 165 |
+
- **Monocytes/Macrophages**: CD14, CD68, LYZ
|
| 166 |
+
- **NK cells**: NCAM1 (CD56), NKG7, KLRD1
|
| 167 |
+
- **Dendritic cells**: FCER1A, CD1C
|
| 168 |
+
|
| 169 |
+
### Epithelial:
|
| 170 |
+
- **General epithelial**: EPCAM, KRT18, KRT19
|
| 171 |
+
- **Lung AT1**: AGER, PDPN
|
| 172 |
+
- **Lung AT2**: SFTPC, SFTPA1
|
| 173 |
+
- **Intestinal**: VIL1, MUC2
|
| 174 |
+
|
| 175 |
+
### Stromal:
|
| 176 |
+
- **Fibroblasts**: COL1A1, DCN, LUM
|
| 177 |
+
- **Endothelial**: PECAM1 (CD31), VWF, CDH5
|
| 178 |
+
- **Smooth muscle**: ACTA2, MYH11, TAGLN
|
| 179 |
+
|
| 180 |
+
## Validation Checklist
|
| 181 |
+
|
| 182 |
+
- [ ] Cluster purity: >80% cells with same label per cluster
|
| 183 |
+
- [ ] Marker consistency: Top DE genes match expected markers
|
| 184 |
+
- [ ] Biological plausibility: Expected proportions for tissue type
|
| 185 |
+
- [ ] Cross-method agreement: Manual and automated annotations align
|
| 186 |
+
- [ ] Reference quality: >70% cells successfully transferred
|
| 187 |
+
- [ ] Doublet check: No clusters with multi-lineage markers
|
| 188 |
+
- [ ] Documentation: Record confidence levels and uncertain calls
|
| 189 |
+
|
| 190 |
+
## Resources
|
| 191 |
+
|
| 192 |
+
### Tools:
|
| 193 |
+
- **Scanpy**: https://scanpy.readthedocs.io/
|
| 194 |
+
- **CellTypist**: https://www.celltypist.org/
|
| 195 |
+
- **scArches**: https://scarches.readthedocs.io/
|
| 196 |
+
- **Seurat**: https://satijalab.org/seurat/
|
| 197 |
+
|
| 198 |
+
### References:
|
| 199 |
+
- **PanglaoDB**: Database of marker genes
|
| 200 |
+
- **CellMarker**: Curated cell marker database
|
| 201 |
+
- **Human Cell Atlas**: Reference datasets
|
| 202 |
+
|
| 203 |
+
### Pre-trained Models:
|
| 204 |
+
- **CellTypist models**: 30+ tissue-specific models
|
| 205 |
+
- **Azimuth references**: PBMC, lung, kidney, etc.
|
| 206 |
+
- **scArches models**: Multiple tissue references
|
| 207 |
+
|
| 208 |
+
## Troubleshooting
|
| 209 |
+
|
| 210 |
+
**Issue**: All clusters look similar
|
| 211 |
+
→ Increase clustering resolution, check if data is normalized
|
| 212 |
+
|
| 213 |
+
**Issue**: Too many small clusters
|
| 214 |
+
→ Decrease resolution, merge similar clusters based on markers
|
| 215 |
+
|
| 216 |
+
**Issue**: Automated tool gives inconsistent results
|
| 217 |
+
→ Check input normalization, try multiple tools, fall back to manual
|
| 218 |
+
|
| 219 |
+
**Issue**: Can't find clear markers for cluster
|
| 220 |
+
→ May be transitional state, doublet, or low-quality cells
|
| 221 |
+
|
| 222 |
+
**Issue**: Reference transfer fails
|
| 223 |
+
→ Check batch correction, ensure overlapping gene sets, verify tissue match
|
Biomanus_upload/biomni/llm.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Newer Claude models can reject an explicit temperature parameter.
|
| 185 |
+
# Omit it and use provider defaults to keep compatibility.
|
| 186 |
+
return ChatAnthropic(
|
| 187 |
+
model=model,
|
| 188 |
+
max_tokens=8192,
|
| 189 |
+
stop_sequences=stop_sequences,
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
elif source == "Gemini":
|
| 193 |
+
# If you want to use ChatGoogleGenerativeAI, you need to pass the stop sequences upon invoking the model.
|
| 194 |
+
# return ChatGoogleGenerativeAI(
|
| 195 |
+
# model=model,
|
| 196 |
+
# temperature=temperature,
|
| 197 |
+
# google_api_key=api_key,
|
| 198 |
+
# )
|
| 199 |
+
try:
|
| 200 |
+
from langchain_openai import ChatOpenAI
|
| 201 |
+
except ImportError:
|
| 202 |
+
raise ImportError( # noqa: B904
|
| 203 |
+
"langchain-openai package is required for Gemini models. Install with: pip install langchain-openai"
|
| 204 |
+
)
|
| 205 |
+
return ChatOpenAI(
|
| 206 |
+
model=model,
|
| 207 |
+
temperature=temperature,
|
| 208 |
+
api_key=os.getenv("GEMINI_API_KEY"),
|
| 209 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
| 210 |
+
stop_sequences=stop_sequences,
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
elif source == "Groq":
|
| 214 |
+
try:
|
| 215 |
+
from langchain_openai import ChatOpenAI
|
| 216 |
+
except ImportError:
|
| 217 |
+
raise ImportError( # noqa: B904
|
| 218 |
+
"langchain-openai package is required for Groq models. Install with: pip install langchain-openai"
|
| 219 |
+
)
|
| 220 |
+
return ChatOpenAI(
|
| 221 |
+
model=model,
|
| 222 |
+
temperature=temperature,
|
| 223 |
+
api_key=os.getenv("GROQ_API_KEY"),
|
| 224 |
+
base_url="https://api.groq.com/openai/v1",
|
| 225 |
+
stop_sequences=stop_sequences,
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
elif source == "Ollama":
|
| 229 |
+
try:
|
| 230 |
+
from langchain_ollama import ChatOllama
|
| 231 |
+
except ImportError:
|
| 232 |
+
raise ImportError( # noqa: B904
|
| 233 |
+
"langchain-ollama package is required for Ollama models. Install with: pip install langchain-ollama"
|
| 234 |
+
)
|
| 235 |
+
return ChatOllama(
|
| 236 |
+
model=model,
|
| 237 |
+
temperature=temperature,
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
elif source == "Bedrock":
|
| 241 |
+
try:
|
| 242 |
+
from langchain_aws import ChatBedrock
|
| 243 |
+
except ImportError:
|
| 244 |
+
raise ImportError( # noqa: B904
|
| 245 |
+
"langchain-aws package is required for Bedrock models. Install with: pip install langchain-aws"
|
| 246 |
+
)
|
| 247 |
+
return ChatBedrock(
|
| 248 |
+
model=model,
|
| 249 |
+
temperature=temperature,
|
| 250 |
+
stop_sequences=stop_sequences,
|
| 251 |
+
region_name=os.getenv("AWS_REGION", "us-east-1"),
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
elif source == "Custom":
|
| 255 |
+
try:
|
| 256 |
+
from langchain_openai import ChatOpenAI
|
| 257 |
+
except ImportError:
|
| 258 |
+
raise ImportError( # noqa: B904
|
| 259 |
+
"langchain-openai package is required for custom models. Install with: pip install langchain-openai"
|
| 260 |
+
)
|
| 261 |
+
# Custom LLM serving such as SGLang. Must expose an openai compatible API.
|
| 262 |
+
assert base_url is not None, "base_url must be provided for customly served LLMs"
|
| 263 |
+
llm = ChatOpenAI(
|
| 264 |
+
model=model,
|
| 265 |
+
temperature=temperature,
|
| 266 |
+
max_tokens=8192,
|
| 267 |
+
stop_sequences=stop_sequences,
|
| 268 |
+
base_url=base_url,
|
| 269 |
+
api_key=api_key,
|
| 270 |
+
)
|
| 271 |
+
return llm
|
| 272 |
+
|
| 273 |
+
else:
|
| 274 |
+
raise ValueError(
|
| 275 |
+
f"Invalid source: {source}. Valid options are 'OpenAI', 'AzureOpenAI', 'Anthropic', 'Gemini', 'Groq', 'Bedrock', or 'Ollama'"
|
| 276 |
+
)
|
Biomanus_upload/biomni/model/__init__.py
ADDED
|
File without changes
|
Biomanus_upload/biomni/model/query_rewriter.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
from langchain_core.messages import HumanMessage
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class QueryRewriter:
|
| 8 |
+
"""Rewrite vague user queries into a retrieval-friendly task description."""
|
| 9 |
+
|
| 10 |
+
STOPWORDS = {
|
| 11 |
+
"a",
|
| 12 |
+
"an",
|
| 13 |
+
"and",
|
| 14 |
+
"are",
|
| 15 |
+
"as",
|
| 16 |
+
"at",
|
| 17 |
+
"be",
|
| 18 |
+
"by",
|
| 19 |
+
"for",
|
| 20 |
+
"from",
|
| 21 |
+
"how",
|
| 22 |
+
"i",
|
| 23 |
+
"in",
|
| 24 |
+
"into",
|
| 25 |
+
"is",
|
| 26 |
+
"it",
|
| 27 |
+
"me",
|
| 28 |
+
"of",
|
| 29 |
+
"on",
|
| 30 |
+
"or",
|
| 31 |
+
"our",
|
| 32 |
+
"please",
|
| 33 |
+
"show",
|
| 34 |
+
"that",
|
| 35 |
+
"the",
|
| 36 |
+
"their",
|
| 37 |
+
"this",
|
| 38 |
+
"to",
|
| 39 |
+
"use",
|
| 40 |
+
"using",
|
| 41 |
+
"want",
|
| 42 |
+
"we",
|
| 43 |
+
"what",
|
| 44 |
+
"with",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
CATEGORY_KEYWORDS = {
|
| 48 |
+
"database_question_answering": {
|
| 49 |
+
"according to",
|
| 50 |
+
"gene set",
|
| 51 |
+
"msigdb",
|
| 52 |
+
"ensembl",
|
| 53 |
+
"clinvar",
|
| 54 |
+
"uniprot",
|
| 55 |
+
"pubmed",
|
| 56 |
+
"gtrd",
|
| 57 |
+
"mirdb",
|
| 58 |
+
"database",
|
| 59 |
+
"located",
|
| 60 |
+
"chromosome",
|
| 61 |
+
"geneid",
|
| 62 |
+
},
|
| 63 |
+
"sequence_question_answering": {
|
| 64 |
+
"sequence",
|
| 65 |
+
"fasta",
|
| 66 |
+
"motif",
|
| 67 |
+
"binding site",
|
| 68 |
+
"primer",
|
| 69 |
+
"reverse complement",
|
| 70 |
+
"codon",
|
| 71 |
+
"peptide",
|
| 72 |
+
"protein sequence",
|
| 73 |
+
"dna sequence",
|
| 74 |
+
"rna sequence",
|
| 75 |
+
"promoter",
|
| 76 |
+
},
|
| 77 |
+
"transcriptomics": {
|
| 78 |
+
"rna",
|
| 79 |
+
"rna-seq",
|
| 80 |
+
"bulk",
|
| 81 |
+
"differential",
|
| 82 |
+
"expression",
|
| 83 |
+
"deseq2",
|
| 84 |
+
"counts",
|
| 85 |
+
},
|
| 86 |
+
"single_cell": {
|
| 87 |
+
"single-cell",
|
| 88 |
+
"singlecell",
|
| 89 |
+
"scrna",
|
| 90 |
+
"scanpy",
|
| 91 |
+
"seurat",
|
| 92 |
+
"cell",
|
| 93 |
+
"cluster",
|
| 94 |
+
},
|
| 95 |
+
"genomics": {
|
| 96 |
+
"genome",
|
| 97 |
+
"variant",
|
| 98 |
+
"vcf",
|
| 99 |
+
"alignment",
|
| 100 |
+
"bwa",
|
| 101 |
+
"gatk",
|
| 102 |
+
"wgs",
|
| 103 |
+
"dna",
|
| 104 |
+
},
|
| 105 |
+
"pathway_enrichment": {
|
| 106 |
+
"gsea",
|
| 107 |
+
"enrichment",
|
| 108 |
+
"pathway",
|
| 109 |
+
"kegg",
|
| 110 |
+
"fgsea",
|
| 111 |
+
"clusterprofiler",
|
| 112 |
+
},
|
| 113 |
+
"statistics": {
|
| 114 |
+
"statistics",
|
| 115 |
+
"test",
|
| 116 |
+
"pvalue",
|
| 117 |
+
"qvalue",
|
| 118 |
+
"regression",
|
| 119 |
+
"significance",
|
| 120 |
+
},
|
| 121 |
+
"literature": {
|
| 122 |
+
"paper",
|
| 123 |
+
"literature",
|
| 124 |
+
"pubmed",
|
| 125 |
+
"publication",
|
| 126 |
+
"article",
|
| 127 |
+
"citation",
|
| 128 |
+
},
|
| 129 |
+
"utility": {
|
| 130 |
+
"json",
|
| 131 |
+
"csv",
|
| 132 |
+
"table",
|
| 133 |
+
"parse",
|
| 134 |
+
"transform",
|
| 135 |
+
"filter",
|
| 136 |
+
"convert",
|
| 137 |
+
"merge",
|
| 138 |
+
},
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
def rewrite(self, query: str, llm=None) -> dict:
|
| 142 |
+
fallback = self._build_fallback(query)
|
| 143 |
+
if llm is None:
|
| 144 |
+
return fallback
|
| 145 |
+
|
| 146 |
+
prompt = f"""
|
| 147 |
+
You rewrite vague biomedical analysis requests into a retrieval-friendly task specification.
|
| 148 |
+
|
| 149 |
+
Return ONLY valid JSON with these keys:
|
| 150 |
+
- task_summary: short sentence
|
| 151 |
+
- rewritten_query: one concise retrieval-oriented query
|
| 152 |
+
- subtasks: list of concrete steps
|
| 153 |
+
- keywords: list of important terms
|
| 154 |
+
- server_hints: list of likely tool or server names if implied
|
| 155 |
+
- categories: list of analysis categories
|
| 156 |
+
- input_requirements: list of required data/input hints
|
| 157 |
+
- output_requirements: list of expected outputs
|
| 158 |
+
- constraints: list of runtime, environment, or quality constraints
|
| 159 |
+
- ambiguities: list of unresolved ambiguities
|
| 160 |
+
|
| 161 |
+
Rules:
|
| 162 |
+
- Preserve the user's intent.
|
| 163 |
+
- Expand vague requests into concrete analysis wording.
|
| 164 |
+
- Mention likely computational steps instead of conversational phrasing.
|
| 165 |
+
- Keep lists short and high-signal.
|
| 166 |
+
|
| 167 |
+
USER QUERY:
|
| 168 |
+
{query}
|
| 169 |
+
"""
|
| 170 |
+
|
| 171 |
+
try:
|
| 172 |
+
response = llm.invoke([HumanMessage(content=prompt)])
|
| 173 |
+
content = self._extract_text(response.content)
|
| 174 |
+
parsed = self._parse_json(content)
|
| 175 |
+
if parsed:
|
| 176 |
+
merged = self._merge_with_fallback(fallback, parsed)
|
| 177 |
+
merged["retrieval_query"] = self._compose_retrieval_query(merged)
|
| 178 |
+
return merged
|
| 179 |
+
except Exception:
|
| 180 |
+
pass
|
| 181 |
+
|
| 182 |
+
return fallback
|
| 183 |
+
|
| 184 |
+
def _build_fallback(self, query: str) -> dict:
|
| 185 |
+
keywords = self._extract_keywords(query)
|
| 186 |
+
categories = self._infer_categories(keywords, query)
|
| 187 |
+
server_hints = self._infer_server_hints(keywords, query)
|
| 188 |
+
subtasks = self._infer_subtasks(query, categories)
|
| 189 |
+
|
| 190 |
+
payload = {
|
| 191 |
+
"original_query": query,
|
| 192 |
+
"task_summary": query.strip(),
|
| 193 |
+
"rewritten_query": query.strip(),
|
| 194 |
+
"subtasks": subtasks,
|
| 195 |
+
"keywords": keywords[:12],
|
| 196 |
+
"server_hints": server_hints[:10],
|
| 197 |
+
"categories": categories[:8],
|
| 198 |
+
"input_requirements": [],
|
| 199 |
+
"output_requirements": [],
|
| 200 |
+
"constraints": [],
|
| 201 |
+
"ambiguities": [],
|
| 202 |
+
}
|
| 203 |
+
payload["retrieval_query"] = self._compose_retrieval_query(payload)
|
| 204 |
+
return payload
|
| 205 |
+
|
| 206 |
+
def _merge_with_fallback(self, fallback: dict, parsed: dict) -> dict:
|
| 207 |
+
merged = dict(fallback)
|
| 208 |
+
for key in (
|
| 209 |
+
"task_summary",
|
| 210 |
+
"rewritten_query",
|
| 211 |
+
"subtasks",
|
| 212 |
+
"keywords",
|
| 213 |
+
"server_hints",
|
| 214 |
+
"categories",
|
| 215 |
+
"input_requirements",
|
| 216 |
+
"output_requirements",
|
| 217 |
+
"constraints",
|
| 218 |
+
"ambiguities",
|
| 219 |
+
):
|
| 220 |
+
value = parsed.get(key)
|
| 221 |
+
if isinstance(value, list):
|
| 222 |
+
merged[key] = [str(item).strip() for item in value if str(item).strip()]
|
| 223 |
+
elif isinstance(value, str) and value.strip():
|
| 224 |
+
merged[key] = value.strip()
|
| 225 |
+
|
| 226 |
+
merged["keywords"] = self._merge_lists(merged.get("keywords", []), fallback.get("keywords", []), limit=14)
|
| 227 |
+
merged["server_hints"] = self._merge_lists(
|
| 228 |
+
merged.get("server_hints", []), fallback.get("server_hints", []), limit=10
|
| 229 |
+
)
|
| 230 |
+
merged["categories"] = self._merge_lists(
|
| 231 |
+
merged.get("categories", []), fallback.get("categories", []), limit=8
|
| 232 |
+
)
|
| 233 |
+
return merged
|
| 234 |
+
|
| 235 |
+
def _compose_retrieval_query(self, payload: dict) -> str:
|
| 236 |
+
parts = [
|
| 237 |
+
payload.get("rewritten_query", ""),
|
| 238 |
+
payload.get("task_summary", ""),
|
| 239 |
+
" ".join(payload.get("categories", [])),
|
| 240 |
+
" ".join(payload.get("keywords", [])),
|
| 241 |
+
" ".join(payload.get("server_hints", [])),
|
| 242 |
+
" ".join(payload.get("subtasks", [])),
|
| 243 |
+
]
|
| 244 |
+
return " | ".join([part for part in parts if part]).strip()
|
| 245 |
+
|
| 246 |
+
def _extract_keywords(self, query: str) -> list[str]:
|
| 247 |
+
tokens = re.findall(r"[A-Za-z0-9_\-\.]{3,}", query.lower())
|
| 248 |
+
keywords = []
|
| 249 |
+
seen = set()
|
| 250 |
+
for token in tokens:
|
| 251 |
+
if token in self.STOPWORDS:
|
| 252 |
+
continue
|
| 253 |
+
if token not in seen:
|
| 254 |
+
seen.add(token)
|
| 255 |
+
keywords.append(token)
|
| 256 |
+
return keywords
|
| 257 |
+
|
| 258 |
+
def _infer_categories(self, keywords: list[str], query: str) -> list[str]:
|
| 259 |
+
text = f"{query.lower()} {' '.join(keywords)}"
|
| 260 |
+
categories = []
|
| 261 |
+
for category, hints in self.CATEGORY_KEYWORDS.items():
|
| 262 |
+
if any(hint in text for hint in hints):
|
| 263 |
+
categories.append(category)
|
| 264 |
+
db_hints = {"according to", "gene set", "msigdb", "ensembl", "clinvar", "uniprot", "mirdb", "gtrd", "omim", "disgenet"}
|
| 265 |
+
enrichment_hints = {"gsea", "enrichment", "pathway", "kegg", "fgsea", "clusterprofiler", "msigdb", "gene set"}
|
| 266 |
+
if "database_question_answering" in categories:
|
| 267 |
+
if "sequence_question_answering" in categories and not any(hint in text for hint in db_hints):
|
| 268 |
+
categories = [cat for cat in categories if cat != "database_question_answering"]
|
| 269 |
+
categories = [cat for cat in categories if cat not in {"transcriptomics", "single_cell"}]
|
| 270 |
+
if "sequence_question_answering" in categories:
|
| 271 |
+
if not any(hint in text for hint in enrichment_hints):
|
| 272 |
+
categories = [cat for cat in categories if cat != "pathway_enrichment"]
|
| 273 |
+
categories = [cat for cat in categories if cat != "single_cell"]
|
| 274 |
+
return categories or ["general_biomedical_analysis"]
|
| 275 |
+
|
| 276 |
+
def _infer_server_hints(self, keywords: list[str], query: str) -> list[str]:
|
| 277 |
+
text = f"{query.lower()} {' '.join(keywords)}"
|
| 278 |
+
hints = []
|
| 279 |
+
known = {
|
| 280 |
+
"deseq2": {"differential", "expression", "rna", "rna-seq", "counts", "deseq2"},
|
| 281 |
+
"fgsea": {"gsea", "fgsea", "enrichment", "ranked"},
|
| 282 |
+
"clusterprofiler": {"go", "kegg", "clusterprofiler", "pathway", "enrichment"},
|
| 283 |
+
"qvalue": {"qvalue", "fdr", "adjusted", "pvalue"},
|
| 284 |
+
"jq": {"json", "jq", "filter", "transform"},
|
| 285 |
+
"bwa": {"bwa", "alignment", "fastq", "genome"},
|
| 286 |
+
"gatk": {"gatk", "variant", "vcf", "germline", "somatic"},
|
| 287 |
+
"scvi": {"scvi", "single-cell", "singlecell", "latent", "integration"},
|
| 288 |
+
"bioconductor-ensembldb": {"ensembl", "chromosome", "geneid", "gene location", "transcript"},
|
| 289 |
+
"ensembl-vep": {"vep", "variant effect", "consequence", "clinvar", "variant annotation"},
|
| 290 |
+
"bioconductor-variantannotation": {"variant annotation", "dbsnp", "vcf", "rsid", "variant"},
|
| 291 |
+
"bioconductor-curatedatlasqueryr": {"cell atlas", "cell type", "tissue", "metadata query"},
|
| 292 |
+
"gseapy": {"gene set", "msigdb", "gsea", "enrichment"},
|
| 293 |
+
"bioconductor-clusterprofiler": {"gene set", "msigdb", "kegg", "go", "ontology"},
|
| 294 |
+
"gimmemotifs": {"motif", "binding site", "tfbs", "gtrd"},
|
| 295 |
+
"blast": {"blast", "homology", "alignment", "sequence"},
|
| 296 |
+
"primer3-py": {"primer", "tm", "amplicon"},
|
| 297 |
+
"pyfaidx": {"fasta", "sequence", "subsequence"},
|
| 298 |
+
"bedtools": {"bed", "interval", "genomic coordinate", "overlap"},
|
| 299 |
+
"samtools": {"samtools", "bam", "faidx", "fasta index"},
|
| 300 |
+
"ucsc-liftover": {"liftover", "assembly", "coordinate"},
|
| 301 |
+
"ucsc-twobittofa": {"2bit", "fasta", "sequence extraction"},
|
| 302 |
+
}
|
| 303 |
+
for name, patterns in known.items():
|
| 304 |
+
if any(pattern in text for pattern in patterns):
|
| 305 |
+
hints.append(name)
|
| 306 |
+
return hints
|
| 307 |
+
|
| 308 |
+
def _infer_subtasks(self, query: str, categories: list[str]) -> list[str]:
|
| 309 |
+
text = query.lower()
|
| 310 |
+
steps = []
|
| 311 |
+
if "database_question_answering" in categories:
|
| 312 |
+
steps.extend(
|
| 313 |
+
[
|
| 314 |
+
"identify the referenced biological database or knowledge source",
|
| 315 |
+
"retrieve the relevant annotation, gene set, or metadata entry",
|
| 316 |
+
"compare the answer choices against the retrieved record",
|
| 317 |
+
]
|
| 318 |
+
)
|
| 319 |
+
if "sequence_question_answering" in categories:
|
| 320 |
+
steps.extend(
|
| 321 |
+
[
|
| 322 |
+
"identify the sequence operation implied by the question",
|
| 323 |
+
"retrieve or derive the required sequence or coordinate fragment",
|
| 324 |
+
"compare the answer choices against the computed sequence features",
|
| 325 |
+
]
|
| 326 |
+
)
|
| 327 |
+
if "transcriptomics" in categories:
|
| 328 |
+
steps.extend(
|
| 329 |
+
[
|
| 330 |
+
"load count or expression matrix",
|
| 331 |
+
"run differential expression analysis",
|
| 332 |
+
]
|
| 333 |
+
)
|
| 334 |
+
if "pathway_enrichment" in categories:
|
| 335 |
+
steps.extend(
|
| 336 |
+
[
|
| 337 |
+
"rank genes or features",
|
| 338 |
+
"run pathway or gene set enrichment",
|
| 339 |
+
]
|
| 340 |
+
)
|
| 341 |
+
if "genomics" in categories:
|
| 342 |
+
steps.extend(
|
| 343 |
+
[
|
| 344 |
+
"prepare reads or aligned files",
|
| 345 |
+
"run alignment or variant calling workflow",
|
| 346 |
+
]
|
| 347 |
+
)
|
| 348 |
+
if "single_cell" in categories:
|
| 349 |
+
steps.extend(
|
| 350 |
+
[
|
| 351 |
+
"load annotated single-cell object",
|
| 352 |
+
"perform clustering or annotation workflow",
|
| 353 |
+
]
|
| 354 |
+
)
|
| 355 |
+
if "literature" in categories or "paper" in text:
|
| 356 |
+
steps.append("retrieve literature or metadata relevant to the study")
|
| 357 |
+
return self._merge_lists(steps, [], limit=8)
|
| 358 |
+
|
| 359 |
+
def _extract_text(self, response_content) -> str:
|
| 360 |
+
if isinstance(response_content, str):
|
| 361 |
+
return response_content
|
| 362 |
+
if isinstance(response_content, list):
|
| 363 |
+
parts = []
|
| 364 |
+
for item in response_content:
|
| 365 |
+
if isinstance(item, dict) and item.get("type") == "text":
|
| 366 |
+
parts.append(str(item.get("text", "")))
|
| 367 |
+
elif isinstance(item, str):
|
| 368 |
+
parts.append(item)
|
| 369 |
+
return "\n".join([part for part in parts if part])
|
| 370 |
+
return str(response_content)
|
| 371 |
+
|
| 372 |
+
def _parse_json(self, text: str) -> dict | None:
|
| 373 |
+
text = text.strip()
|
| 374 |
+
if not text:
|
| 375 |
+
return None
|
| 376 |
+
try:
|
| 377 |
+
return json.loads(text)
|
| 378 |
+
except json.JSONDecodeError:
|
| 379 |
+
match = re.search(r"\{[\s\S]*\}", text)
|
| 380 |
+
if not match:
|
| 381 |
+
return None
|
| 382 |
+
try:
|
| 383 |
+
return json.loads(match.group(0))
|
| 384 |
+
except json.JSONDecodeError:
|
| 385 |
+
return None
|
| 386 |
+
|
| 387 |
+
def _merge_lists(self, primary: list[str], secondary: list[str], limit: int) -> list[str]:
|
| 388 |
+
merged = []
|
| 389 |
+
seen = set()
|
| 390 |
+
for item in list(primary) + list(secondary):
|
| 391 |
+
normalized = str(item).strip()
|
| 392 |
+
lowered = normalized.lower()
|
| 393 |
+
if not normalized or lowered in seen:
|
| 394 |
+
continue
|
| 395 |
+
seen.add(lowered)
|
| 396 |
+
merged.append(normalized)
|
| 397 |
+
if len(merged) >= limit:
|
| 398 |
+
break
|
| 399 |
+
return merged
|
Biomanus_upload/biomni/model/retriever.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import contextlib
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
from langchain_core.messages import HumanMessage
|
| 5 |
+
from langchain_openai import ChatOpenAI
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class ToolRetriever:
|
| 9 |
+
"""Retrieve tools from the tool registry."""
|
| 10 |
+
|
| 11 |
+
SERVER_CATEGORY_HINTS = {
|
| 12 |
+
"transcriptomics": {
|
| 13 |
+
"deseq2",
|
| 14 |
+
"rna",
|
| 15 |
+
"rnaseq",
|
| 16 |
+
"bulk",
|
| 17 |
+
"expression",
|
| 18 |
+
"transcript",
|
| 19 |
+
},
|
| 20 |
+
"pathway_enrichment": {
|
| 21 |
+
"fgsea",
|
| 22 |
+
"gsea",
|
| 23 |
+
"clusterprofiler",
|
| 24 |
+
"pathway",
|
| 25 |
+
"go",
|
| 26 |
+
"kegg",
|
| 27 |
+
"reactome",
|
| 28 |
+
},
|
| 29 |
+
"single_cell": {
|
| 30 |
+
"singlecell",
|
| 31 |
+
"single-cell",
|
| 32 |
+
"scanpy",
|
| 33 |
+
"seurat",
|
| 34 |
+
"scvi",
|
| 35 |
+
"cell",
|
| 36 |
+
"neighborhood",
|
| 37 |
+
},
|
| 38 |
+
"genomics": {
|
| 39 |
+
"bwa",
|
| 40 |
+
"gatk",
|
| 41 |
+
"variant",
|
| 42 |
+
"genome",
|
| 43 |
+
"wgs",
|
| 44 |
+
"fastq",
|
| 45 |
+
"alignment",
|
| 46 |
+
},
|
| 47 |
+
"utility": {
|
| 48 |
+
"jq",
|
| 49 |
+
"json",
|
| 50 |
+
"csv",
|
| 51 |
+
"convert",
|
| 52 |
+
"parser",
|
| 53 |
+
"transform",
|
| 54 |
+
},
|
| 55 |
+
"literature": {
|
| 56 |
+
"pubmed",
|
| 57 |
+
"paper",
|
| 58 |
+
"literature",
|
| 59 |
+
"citation",
|
| 60 |
+
},
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
def __init__(self):
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
def prompt_based_retrieval(self, query: str, resources: dict, llm=None) -> dict:
|
| 67 |
+
"""Use a prompt-based approach to retrieve the most relevant resources for a query.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
query: The user's query
|
| 71 |
+
resources: A dictionary with keys 'tools', 'data_lake', 'libraries', and 'know_how',
|
| 72 |
+
each containing a list of available resources
|
| 73 |
+
llm: Optional LLM instance to use for retrieval (if None, will create a new one)
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
A dictionary with the same keys, but containing only the most relevant resources
|
| 77 |
+
|
| 78 |
+
"""
|
| 79 |
+
# Build prompt sections for available resources
|
| 80 |
+
prompt_sections = []
|
| 81 |
+
prompt_sections.append(f"""
|
| 82 |
+
You are an expert biomedical research assistant. Your task is to select the relevant resources to help answer a user's query.
|
| 83 |
+
|
| 84 |
+
USER QUERY: {query}
|
| 85 |
+
|
| 86 |
+
Below are the available resources. For each category, select items that are directly or indirectly relevant to answering the query.
|
| 87 |
+
Be generous in your selection - include resources that might be useful for the task, even if they're not explicitly mentioned in the query.
|
| 88 |
+
It's better to include slightly more resources than to miss potentially useful ones.
|
| 89 |
+
|
| 90 |
+
AVAILABLE TOOLS:
|
| 91 |
+
{self._format_resources_for_prompt(resources.get("tools", []))}
|
| 92 |
+
|
| 93 |
+
AVAILABLE DATA LAKE ITEMS:
|
| 94 |
+
{self._format_resources_for_prompt(resources.get("data_lake", []))}
|
| 95 |
+
|
| 96 |
+
AVAILABLE SOFTWARE LIBRARIES:
|
| 97 |
+
{self._format_resources_for_prompt(resources.get("libraries", []))}""")
|
| 98 |
+
|
| 99 |
+
# Add know-how section if available
|
| 100 |
+
if "know_how" in resources and resources["know_how"]:
|
| 101 |
+
prompt_sections.append(f"""
|
| 102 |
+
AVAILABLE KNOW-HOW DOCUMENTS (Best Practices & Protocols):
|
| 103 |
+
{self._format_resources_for_prompt(resources.get("know_how", []))}""")
|
| 104 |
+
|
| 105 |
+
# Build response format based on available categories
|
| 106 |
+
response_format = """
|
| 107 |
+
For each category, respond with ONLY the indices of the relevant items in the following format:
|
| 108 |
+
TOOLS: [list of indices]
|
| 109 |
+
DATA_LAKE: [list of indices]
|
| 110 |
+
LIBRARIES: [list of indices]"""
|
| 111 |
+
|
| 112 |
+
if "know_how" in resources and resources["know_how"]:
|
| 113 |
+
response_format += "\nKNOW_HOW: [list of indices]"
|
| 114 |
+
|
| 115 |
+
response_format += """
|
| 116 |
+
|
| 117 |
+
For example:
|
| 118 |
+
TOOLS: [0, 3, 5, 7, 9]
|
| 119 |
+
DATA_LAKE: [1, 2, 4]
|
| 120 |
+
LIBRARIES: [0, 2, 4, 5, 8]"""
|
| 121 |
+
|
| 122 |
+
if "know_how" in resources and resources["know_how"]:
|
| 123 |
+
response_format += "\nKNOW_HOW: [0, 1]"
|
| 124 |
+
|
| 125 |
+
response_format += """
|
| 126 |
+
|
| 127 |
+
If a category has no relevant items, use an empty list, e.g., DATA_LAKE: []
|
| 128 |
+
|
| 129 |
+
IMPORTANT GUIDELINES:
|
| 130 |
+
1. Be generous but not excessive - aim to include all potentially relevant resources
|
| 131 |
+
2. ALWAYS prioritize database tools for general queries - include as many database tools as possible
|
| 132 |
+
3. Include all literature search tools
|
| 133 |
+
4. For wet lab sequence type of queries, ALWAYS include molecular biology tools
|
| 134 |
+
5. For data lake items, include datasets that could provide useful information
|
| 135 |
+
6. For libraries, include those that provide functions needed for analysis
|
| 136 |
+
7. For know-how documents, include those that provide relevant protocols, best practices, or troubleshooting guidance
|
| 137 |
+
8. Don't exclude resources just because they're not explicitly mentioned in the query
|
| 138 |
+
9. When in doubt about a database tool or molecular biology tool, include it rather than exclude it
|
| 139 |
+
"""
|
| 140 |
+
|
| 141 |
+
prompt = "\n".join(prompt_sections) + response_format
|
| 142 |
+
|
| 143 |
+
# Use the provided LLM or create a new one
|
| 144 |
+
if llm is None:
|
| 145 |
+
llm = ChatOpenAI(model="gpt-4o")
|
| 146 |
+
|
| 147 |
+
# Invoke the LLM
|
| 148 |
+
if hasattr(llm, "invoke"):
|
| 149 |
+
# For LangChain-style LLMs
|
| 150 |
+
response = llm.invoke([HumanMessage(content=prompt)])
|
| 151 |
+
response_content = response.content
|
| 152 |
+
else:
|
| 153 |
+
# For other LLM interfaces
|
| 154 |
+
response_content = str(llm(prompt))
|
| 155 |
+
|
| 156 |
+
# Parse the response to extract the selected indices
|
| 157 |
+
selected_indices = self._parse_llm_response(response_content)
|
| 158 |
+
|
| 159 |
+
# Get the selected resources
|
| 160 |
+
selected_resources = {
|
| 161 |
+
"tools": [
|
| 162 |
+
resources["tools"][i] for i in selected_indices.get("tools", []) if i < len(resources.get("tools", []))
|
| 163 |
+
],
|
| 164 |
+
"data_lake": [
|
| 165 |
+
resources["data_lake"][i]
|
| 166 |
+
for i in selected_indices.get("data_lake", [])
|
| 167 |
+
if i < len(resources.get("data_lake", []))
|
| 168 |
+
],
|
| 169 |
+
"libraries": [
|
| 170 |
+
resources["libraries"][i]
|
| 171 |
+
for i in selected_indices.get("libraries", [])
|
| 172 |
+
if i < len(resources.get("libraries", []))
|
| 173 |
+
],
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
# Add know-how if present
|
| 177 |
+
if "know_how" in resources and resources["know_how"]:
|
| 178 |
+
selected_resources["know_how"] = [
|
| 179 |
+
resources["know_how"][i]
|
| 180 |
+
for i in selected_indices.get("know_how", [])
|
| 181 |
+
if i < len(resources.get("know_how", []))
|
| 182 |
+
]
|
| 183 |
+
|
| 184 |
+
return selected_resources
|
| 185 |
+
|
| 186 |
+
def _format_resources_for_prompt(self, resources: list) -> str:
|
| 187 |
+
"""Format resources for inclusion in the prompt."""
|
| 188 |
+
formatted = []
|
| 189 |
+
for i, resource in enumerate(resources):
|
| 190 |
+
if isinstance(resource, dict):
|
| 191 |
+
# Handle dictionary format (from tool registry or data lake/libraries with descriptions)
|
| 192 |
+
name = resource.get("name", f"Resource {i}")
|
| 193 |
+
description = resource.get("description", "")
|
| 194 |
+
formatted.append(f"{i}. {name}: {description}")
|
| 195 |
+
elif isinstance(resource, str):
|
| 196 |
+
# Handle string format (simple strings)
|
| 197 |
+
formatted.append(f"{i}. {resource}")
|
| 198 |
+
else:
|
| 199 |
+
# Try to extract name and description from tool objects
|
| 200 |
+
name = getattr(resource, "name", str(resource))
|
| 201 |
+
desc = getattr(resource, "description", "")
|
| 202 |
+
formatted.append(f"{i}. {name}: {desc}")
|
| 203 |
+
|
| 204 |
+
return "\n".join(formatted) if formatted else "None available"
|
| 205 |
+
|
| 206 |
+
def _parse_llm_response(self, response) -> dict:
|
| 207 |
+
"""Parse the LLM response to extract the selected indices.
|
| 208 |
+
|
| 209 |
+
Accepts either a plain string or a Responses API-style list of content blocks.
|
| 210 |
+
"""
|
| 211 |
+
# Normalize response to string if it's a list of content blocks (Responses API)
|
| 212 |
+
if isinstance(response, list):
|
| 213 |
+
parts = []
|
| 214 |
+
for item in response:
|
| 215 |
+
# LangChain Responses API returns list of dicts like {"type": "text", "text": "..."}
|
| 216 |
+
if isinstance(item, dict):
|
| 217 |
+
if item.get("type") == "text" and "text" in item:
|
| 218 |
+
parts.append(str(item.get("text", "")))
|
| 219 |
+
# If it's a tool_call or other block, ignore for this simple parsing
|
| 220 |
+
elif isinstance(item, str):
|
| 221 |
+
parts.append(item)
|
| 222 |
+
response = "\n".join([p for p in parts if p])
|
| 223 |
+
elif not isinstance(response, str):
|
| 224 |
+
response = str(response)
|
| 225 |
+
selected_indices = {"tools": [], "data_lake": [], "libraries": [], "know_how": []}
|
| 226 |
+
|
| 227 |
+
# Extract indices for each category
|
| 228 |
+
tools_match = re.search(r"TOOLS:\s*\[(.*?)\]", response, re.IGNORECASE)
|
| 229 |
+
if tools_match and tools_match.group(1).strip():
|
| 230 |
+
with contextlib.suppress(ValueError):
|
| 231 |
+
selected_indices["tools"] = [int(idx.strip()) for idx in tools_match.group(1).split(",") if idx.strip()]
|
| 232 |
+
|
| 233 |
+
data_lake_match = re.search(r"DATA_LAKE:\s*\[(.*?)\]", response, re.IGNORECASE)
|
| 234 |
+
if data_lake_match and data_lake_match.group(1).strip():
|
| 235 |
+
with contextlib.suppress(ValueError):
|
| 236 |
+
selected_indices["data_lake"] = [
|
| 237 |
+
int(idx.strip()) for idx in data_lake_match.group(1).split(",") if idx.strip()
|
| 238 |
+
]
|
| 239 |
+
|
| 240 |
+
libraries_match = re.search(r"LIBRARIES:\s*\[(.*?)\]", response, re.IGNORECASE)
|
| 241 |
+
if libraries_match and libraries_match.group(1).strip():
|
| 242 |
+
with contextlib.suppress(ValueError):
|
| 243 |
+
selected_indices["libraries"] = [
|
| 244 |
+
int(idx.strip()) for idx in libraries_match.group(1).split(",") if idx.strip()
|
| 245 |
+
]
|
| 246 |
+
|
| 247 |
+
# Extract know-how indices
|
| 248 |
+
know_how_match = re.search(r"KNOW[-_]HOW:\s*\[(.*?)\]", response, re.IGNORECASE)
|
| 249 |
+
if know_how_match and know_how_match.group(1).strip():
|
| 250 |
+
with contextlib.suppress(ValueError):
|
| 251 |
+
selected_indices["know_how"] = [
|
| 252 |
+
int(idx.strip()) for idx in know_how_match.group(1).split(",") if idx.strip()
|
| 253 |
+
]
|
| 254 |
+
|
| 255 |
+
return selected_indices
|
| 256 |
+
|
| 257 |
+
def build_mcp_server_entry(self, server_name: str, safe_server_name: str, tools: list, server_meta: dict) -> dict:
|
| 258 |
+
"""Build a retrieval document for one MCP server."""
|
| 259 |
+
normalized_tools = []
|
| 260 |
+
for tool in tools:
|
| 261 |
+
if not isinstance(tool, dict):
|
| 262 |
+
continue
|
| 263 |
+
normalized_tools.append(
|
| 264 |
+
{
|
| 265 |
+
"name": tool.get("name") or tool.get("biomni_name") or "unknown_tool",
|
| 266 |
+
"description": tool.get("description", ""),
|
| 267 |
+
"inputSchema": tool.get(
|
| 268 |
+
"inputSchema",
|
| 269 |
+
{"properties": tool.get("parameters", {}), "required": []},
|
| 270 |
+
),
|
| 271 |
+
}
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
combined_text = " ".join(
|
| 275 |
+
[
|
| 276 |
+
server_name,
|
| 277 |
+
safe_server_name,
|
| 278 |
+
server_meta.get("description", ""),
|
| 279 |
+
" ".join(tool.get("name", "") for tool in normalized_tools),
|
| 280 |
+
" ".join(tool.get("description", "") for tool in normalized_tools),
|
| 281 |
+
]
|
| 282 |
+
)
|
| 283 |
+
tokens = self._tokenize(combined_text)
|
| 284 |
+
category = self._infer_server_category(combined_text, tokens)
|
| 285 |
+
summary = (
|
| 286 |
+
server_meta.get("description")
|
| 287 |
+
or f"MCP server {server_name} with tools: "
|
| 288 |
+
+ ", ".join(tool.get("name", "") for tool in normalized_tools[:8])
|
| 289 |
+
).strip()
|
| 290 |
+
|
| 291 |
+
return {
|
| 292 |
+
"name": server_name,
|
| 293 |
+
"safe_name": safe_server_name,
|
| 294 |
+
"module": f"mcp_servers.{safe_server_name}",
|
| 295 |
+
"category": category,
|
| 296 |
+
"summary": summary,
|
| 297 |
+
"keywords": list(tokens)[:40],
|
| 298 |
+
"tools": normalized_tools,
|
| 299 |
+
"tool_names": [tool["name"] for tool in normalized_tools],
|
| 300 |
+
"server_meta": server_meta,
|
| 301 |
+
"score": 0.0,
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
def hybrid_server_retrieval(
|
| 305 |
+
self,
|
| 306 |
+
query_context: dict,
|
| 307 |
+
server_entries: list[dict],
|
| 308 |
+
llm=None,
|
| 309 |
+
top_k: int = 8,
|
| 310 |
+
candidate_pool: int = 24,
|
| 311 |
+
) -> list[dict]:
|
| 312 |
+
"""Select relevant MCP servers with lexical scoring plus optional LLM reranking."""
|
| 313 |
+
if not server_entries:
|
| 314 |
+
return []
|
| 315 |
+
|
| 316 |
+
scored = []
|
| 317 |
+
for idx, entry in enumerate(server_entries):
|
| 318 |
+
score = self._score_server_entry(query_context, entry)
|
| 319 |
+
enriched = dict(entry)
|
| 320 |
+
enriched["score"] = score
|
| 321 |
+
scored.append((score, idx, enriched))
|
| 322 |
+
|
| 323 |
+
scored.sort(key=lambda item: item[0], reverse=True)
|
| 324 |
+
shortlisted = [entry for _, _, entry in scored[: max(top_k, candidate_pool)]]
|
| 325 |
+
|
| 326 |
+
selected = self._llm_rerank_servers(query_context, shortlisted, top_k=top_k, llm=llm)
|
| 327 |
+
if selected:
|
| 328 |
+
return selected
|
| 329 |
+
return shortlisted[:top_k]
|
| 330 |
+
|
| 331 |
+
def hybrid_tool_selection(
|
| 332 |
+
self,
|
| 333 |
+
query_context: dict,
|
| 334 |
+
tools: list[dict],
|
| 335 |
+
llm=None,
|
| 336 |
+
top_k: int = 12,
|
| 337 |
+
) -> list[dict]:
|
| 338 |
+
"""Select high-signal tools inside a shortlisted MCP server."""
|
| 339 |
+
if len(tools) <= top_k:
|
| 340 |
+
return tools
|
| 341 |
+
|
| 342 |
+
scored = []
|
| 343 |
+
for idx, tool in enumerate(tools):
|
| 344 |
+
score = self._score_tool_entry(query_context, tool)
|
| 345 |
+
scored.append((score, idx, tool))
|
| 346 |
+
scored.sort(key=lambda item: item[0], reverse=True)
|
| 347 |
+
shortlisted = [tool for _, _, tool in scored[: max(top_k * 2, top_k)]]
|
| 348 |
+
|
| 349 |
+
selected = self._llm_rerank_tools(query_context, shortlisted, top_k=top_k, llm=llm)
|
| 350 |
+
if selected:
|
| 351 |
+
return selected
|
| 352 |
+
return shortlisted[:top_k]
|
| 353 |
+
|
| 354 |
+
def _score_server_entry(self, query_context: dict, entry: dict) -> float:
|
| 355 |
+
query_tokens = self._tokenize(self._query_text(query_context))
|
| 356 |
+
if not query_tokens:
|
| 357 |
+
return 0.0
|
| 358 |
+
|
| 359 |
+
server_tokens = set(entry.get("keywords", [])) | self._tokenize(entry.get("summary", ""))
|
| 360 |
+
overlap = len(query_tokens & server_tokens)
|
| 361 |
+
score = float(overlap)
|
| 362 |
+
|
| 363 |
+
server_name = entry.get("name", "").lower()
|
| 364 |
+
category = entry.get("category", "").lower()
|
| 365 |
+
|
| 366 |
+
for hint in query_context.get("server_hints", []):
|
| 367 |
+
hint_l = str(hint).lower()
|
| 368 |
+
if hint_l and hint_l in server_name:
|
| 369 |
+
score += 5.0
|
| 370 |
+
|
| 371 |
+
for category_hint in query_context.get("categories", []):
|
| 372 |
+
if str(category_hint).lower() == category:
|
| 373 |
+
score += 3.0
|
| 374 |
+
|
| 375 |
+
tool_names = " ".join(entry.get("tool_names", [])).lower()
|
| 376 |
+
for token in query_tokens:
|
| 377 |
+
if token in server_name:
|
| 378 |
+
score += 2.5
|
| 379 |
+
if token in tool_names:
|
| 380 |
+
score += 1.0
|
| 381 |
+
|
| 382 |
+
return score
|
| 383 |
+
|
| 384 |
+
def _score_tool_entry(self, query_context: dict, tool: dict) -> float:
|
| 385 |
+
query_tokens = self._tokenize(self._query_text(query_context))
|
| 386 |
+
tool_text = " ".join([tool.get("name", ""), tool.get("description", "")])
|
| 387 |
+
tool_tokens = self._tokenize(tool_text)
|
| 388 |
+
overlap = len(query_tokens & tool_tokens)
|
| 389 |
+
score = float(overlap)
|
| 390 |
+
|
| 391 |
+
for token in query_tokens:
|
| 392 |
+
if token in tool.get("name", "").lower():
|
| 393 |
+
score += 2.0
|
| 394 |
+
|
| 395 |
+
for hint in query_context.get("server_hints", []):
|
| 396 |
+
hint_l = str(hint).lower()
|
| 397 |
+
if hint_l and hint_l in tool_text.lower():
|
| 398 |
+
score += 2.5
|
| 399 |
+
|
| 400 |
+
return score
|
| 401 |
+
|
| 402 |
+
def _llm_rerank_servers(self, query_context: dict, candidates: list[dict], top_k: int, llm=None) -> list[dict]:
|
| 403 |
+
if llm is None or not candidates:
|
| 404 |
+
return []
|
| 405 |
+
|
| 406 |
+
lines = []
|
| 407 |
+
for idx, candidate in enumerate(candidates):
|
| 408 |
+
lines.append(
|
| 409 |
+
f"{idx}. {candidate['name']} | category={candidate.get('category', 'unknown')} | "
|
| 410 |
+
f"tools={', '.join(candidate.get('tool_names', [])[:8])} | summary={candidate.get('summary', '')}"
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
prompt = f"""
|
| 414 |
+
Select the most relevant MCP servers for this query.
|
| 415 |
+
|
| 416 |
+
QUERY:
|
| 417 |
+
{self._query_text(query_context)}
|
| 418 |
+
|
| 419 |
+
SERVER CANDIDATES:
|
| 420 |
+
{chr(10).join(lines)}
|
| 421 |
+
|
| 422 |
+
Return ONLY:
|
| 423 |
+
SERVERS: [indices]
|
| 424 |
+
|
| 425 |
+
Choose at most {top_k} servers.
|
| 426 |
+
Prefer precise servers over broad but irrelevant ones.
|
| 427 |
+
"""
|
| 428 |
+
try:
|
| 429 |
+
response = llm.invoke([HumanMessage(content=prompt)])
|
| 430 |
+
content = response.content if hasattr(response, "content") else str(response)
|
| 431 |
+
text = self._normalize_text(content)
|
| 432 |
+
match = re.search(r"SERVERS:\s*\[(.*?)\]", text, re.IGNORECASE)
|
| 433 |
+
if not match:
|
| 434 |
+
return []
|
| 435 |
+
indices = [int(idx.strip()) for idx in match.group(1).split(",") if idx.strip()]
|
| 436 |
+
return [candidates[idx] for idx in indices if 0 <= idx < len(candidates)]
|
| 437 |
+
except Exception:
|
| 438 |
+
return []
|
| 439 |
+
|
| 440 |
+
def _llm_rerank_tools(self, query_context: dict, candidates: list[dict], top_k: int, llm=None) -> list[dict]:
|
| 441 |
+
if llm is None or not candidates:
|
| 442 |
+
return []
|
| 443 |
+
|
| 444 |
+
lines = []
|
| 445 |
+
for idx, tool in enumerate(candidates):
|
| 446 |
+
lines.append(f"{idx}. {tool.get('name', 'unknown')}: {tool.get('description', '')}")
|
| 447 |
+
|
| 448 |
+
prompt = f"""
|
| 449 |
+
Select the most relevant MCP tools for this query.
|
| 450 |
+
|
| 451 |
+
QUERY:
|
| 452 |
+
{self._query_text(query_context)}
|
| 453 |
+
|
| 454 |
+
TOOL CANDIDATES:
|
| 455 |
+
{chr(10).join(lines)}
|
| 456 |
+
|
| 457 |
+
Return ONLY:
|
| 458 |
+
TOOLS: [indices]
|
| 459 |
+
|
| 460 |
+
Choose at most {top_k} tools.
|
| 461 |
+
"""
|
| 462 |
+
try:
|
| 463 |
+
response = llm.invoke([HumanMessage(content=prompt)])
|
| 464 |
+
content = response.content if hasattr(response, "content") else str(response)
|
| 465 |
+
text = self._normalize_text(content)
|
| 466 |
+
match = re.search(r"TOOLS:\s*\[(.*?)\]", text, re.IGNORECASE)
|
| 467 |
+
if not match:
|
| 468 |
+
return []
|
| 469 |
+
indices = [int(idx.strip()) for idx in match.group(1).split(",") if idx.strip()]
|
| 470 |
+
return [candidates[idx] for idx in indices if 0 <= idx < len(candidates)]
|
| 471 |
+
except Exception:
|
| 472 |
+
return []
|
| 473 |
+
|
| 474 |
+
def _infer_server_category(self, text: str, tokens: set[str]) -> str:
|
| 475 |
+
lowered = text.lower()
|
| 476 |
+
for category, hints in self.SERVER_CATEGORY_HINTS.items():
|
| 477 |
+
if any(hint in lowered for hint in hints) or tokens.intersection(hints):
|
| 478 |
+
return category
|
| 479 |
+
return "general"
|
| 480 |
+
|
| 481 |
+
def _query_text(self, query_context: dict) -> str:
|
| 482 |
+
if not isinstance(query_context, dict):
|
| 483 |
+
return str(query_context)
|
| 484 |
+
return query_context.get("retrieval_query") or query_context.get("rewritten_query") or query_context.get(
|
| 485 |
+
"original_query", ""
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
def _tokenize(self, text: str) -> set[str]:
|
| 489 |
+
return {token for token in re.findall(r"[a-zA-Z0-9_\-\.]{3,}", str(text).lower())}
|
| 490 |
+
|
| 491 |
+
def _normalize_text(self, content) -> str:
|
| 492 |
+
if isinstance(content, str):
|
| 493 |
+
return content
|
| 494 |
+
if isinstance(content, list):
|
| 495 |
+
parts = []
|
| 496 |
+
for item in content:
|
| 497 |
+
if isinstance(item, dict) and item.get("type") == "text":
|
| 498 |
+
parts.append(str(item.get("text", "")))
|
| 499 |
+
elif isinstance(item, str):
|
| 500 |
+
parts.append(item)
|
| 501 |
+
return "\n".join([part for part in parts if part])
|
| 502 |
+
return str(content)
|
Biomanus_upload/biomni/task/__init__.py
ADDED
|
File without changes
|
Biomanus_upload/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"]
|
Biomanus_upload/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
|
Biomanus_upload/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
|
Biomanus_upload/biomni/tool/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
from biomni.utils import get_tool_decorated_functions # noqa: F401
|
| 3 |
+
except ModuleNotFoundError: # pragma: no cover
|
| 4 |
+
get_tool_decorated_functions = None # type: ignore
|
Biomanus_upload/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)
|
Biomanus_upload/biomni/tool/bioengineering.py
ADDED
|
@@ -0,0 +1,1387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def analyze_cell_migration_metrics(
|
| 2 |
+
image_sequence_path,
|
| 3 |
+
pixel_size_um=1.0,
|
| 4 |
+
time_interval_min=1.0,
|
| 5 |
+
min_track_length=10,
|
| 6 |
+
output_dir="./",
|
| 7 |
+
):
|
| 8 |
+
"""Analyze cell migration metrics from time-lapse microscopy images.
|
| 9 |
+
|
| 10 |
+
Parameters
|
| 11 |
+
----------
|
| 12 |
+
image_sequence_path : str
|
| 13 |
+
Path to the directory containing time-lapse images or path to a multi-frame TIFF file
|
| 14 |
+
pixel_size_um : float
|
| 15 |
+
Conversion factor from pixels to micrometers (default: 1.0)
|
| 16 |
+
time_interval_min : float
|
| 17 |
+
Time interval between consecutive frames in minutes (default: 1.0)
|
| 18 |
+
min_track_length : int
|
| 19 |
+
Minimum number of frames a cell must be tracked to be included in analysis (default: 10)
|
| 20 |
+
output_dir : str
|
| 21 |
+
Directory to save output files (default: "./")
|
| 22 |
+
|
| 23 |
+
Returns
|
| 24 |
+
-------
|
| 25 |
+
str
|
| 26 |
+
Research log summarizing the cell migration analysis process and results
|
| 27 |
+
|
| 28 |
+
"""
|
| 29 |
+
import os
|
| 30 |
+
|
| 31 |
+
import matplotlib.pyplot as plt
|
| 32 |
+
import numpy as np
|
| 33 |
+
import pandas as pd
|
| 34 |
+
import trackpy as tp
|
| 35 |
+
from skimage import io
|
| 36 |
+
|
| 37 |
+
# Create output directory if it doesn't exist
|
| 38 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 39 |
+
|
| 40 |
+
# Load image sequence
|
| 41 |
+
if os.path.isdir(image_sequence_path):
|
| 42 |
+
# Load from directory of images
|
| 43 |
+
image_files = sorted(
|
| 44 |
+
[f for f in os.listdir(image_sequence_path) if f.endswith((".tif", ".tiff", ".png", ".jpg", ".jpeg"))]
|
| 45 |
+
)
|
| 46 |
+
frames = [io.imread(os.path.join(image_sequence_path, f)) for f in image_files]
|
| 47 |
+
else:
|
| 48 |
+
# Load multi-frame TIFF
|
| 49 |
+
frames = io.imread(image_sequence_path)
|
| 50 |
+
if frames.ndim == 3: # Check if it's a time series
|
| 51 |
+
pass
|
| 52 |
+
else:
|
| 53 |
+
return "Error: Input is not a valid time-lapse image sequence"
|
| 54 |
+
|
| 55 |
+
# Step 1: Detect cells in each frame
|
| 56 |
+
features_list = []
|
| 57 |
+
for i, frame in enumerate(frames):
|
| 58 |
+
# Detect cells (particles) in the frame
|
| 59 |
+
features = tp.locate(frame, diameter=15, minmass=100)
|
| 60 |
+
if features is not None and not features.empty:
|
| 61 |
+
features["frame"] = i
|
| 62 |
+
features_list.append(features)
|
| 63 |
+
|
| 64 |
+
if not features_list:
|
| 65 |
+
return "Error: No cells detected in the image sequence"
|
| 66 |
+
|
| 67 |
+
# Combine all features
|
| 68 |
+
all_features = pd.concat(features_list)
|
| 69 |
+
|
| 70 |
+
# Save raw detections to output directory
|
| 71 |
+
raw_detections_file = os.path.join(output_dir, "raw_cell_detections.csv")
|
| 72 |
+
all_features.to_csv(raw_detections_file, index=False)
|
| 73 |
+
|
| 74 |
+
# Step 2: Link features into cell trajectories
|
| 75 |
+
trajectories = tp.link_df(all_features, search_range=10, memory=3)
|
| 76 |
+
|
| 77 |
+
# Save linked trajectories before filtering
|
| 78 |
+
# Reset index to ensure frame is a column, not an index level
|
| 79 |
+
trajectories_reset = trajectories.reset_index(drop=True)
|
| 80 |
+
all_trajectories_file = os.path.join(output_dir, "all_trajectories.csv")
|
| 81 |
+
trajectories_reset.to_csv(all_trajectories_file, index=False)
|
| 82 |
+
|
| 83 |
+
# Step 3: Filter trajectories to get only the ones that appear in enough frames
|
| 84 |
+
trajectories = tp.filter_stubs(trajectories, threshold=min_track_length)
|
| 85 |
+
|
| 86 |
+
if trajectories.empty:
|
| 87 |
+
return "No complete cell tracks found. Try adjusting parameters."
|
| 88 |
+
|
| 89 |
+
# Save filtered trajectories
|
| 90 |
+
filtered_trajectories_file = os.path.join(output_dir, "filtered_trajectories.csv")
|
| 91 |
+
# Reset index again to be safe
|
| 92 |
+
trajectories = trajectories.reset_index(drop=True)
|
| 93 |
+
trajectories.to_csv(filtered_trajectories_file, index=False)
|
| 94 |
+
|
| 95 |
+
# Step 4: Calculate migration metrics for each cell
|
| 96 |
+
cell_ids = trajectories["particle"].unique()
|
| 97 |
+
metrics = []
|
| 98 |
+
|
| 99 |
+
for cell_id in cell_ids:
|
| 100 |
+
cell_track = trajectories[trajectories["particle"] == cell_id].sort_values("frame")
|
| 101 |
+
|
| 102 |
+
# Convert pixel positions to micrometers
|
| 103 |
+
cell_track["x_um"] = cell_track["x"] * pixel_size_um
|
| 104 |
+
cell_track["y_um"] = cell_track["y"] * pixel_size_um
|
| 105 |
+
|
| 106 |
+
# Calculate displacements between consecutive frames
|
| 107 |
+
dx = np.diff(cell_track["x_um"])
|
| 108 |
+
dy = np.diff(cell_track["y_um"])
|
| 109 |
+
|
| 110 |
+
# Calculate step distances
|
| 111 |
+
step_distances = np.sqrt(dx**2 + dy**2)
|
| 112 |
+
|
| 113 |
+
# Calculate total path length
|
| 114 |
+
path_length = np.sum(step_distances)
|
| 115 |
+
|
| 116 |
+
# Calculate net displacement (straight-line distance from start to end)
|
| 117 |
+
start_x, start_y = cell_track.iloc[0][["x_um", "y_um"]]
|
| 118 |
+
end_x, end_y = cell_track.iloc[-1][["x_um", "y_um"]]
|
| 119 |
+
net_displacement = np.sqrt((end_x - start_x) ** 2 + (end_y - start_y) ** 2)
|
| 120 |
+
|
| 121 |
+
# Calculate directionality ratio (net displacement / path length)
|
| 122 |
+
directionality = net_displacement / path_length if path_length > 0 else 0
|
| 123 |
+
|
| 124 |
+
# Calculate speed (μm/min)
|
| 125 |
+
time_tracked = (cell_track["frame"].max() - cell_track["frame"].min()) * time_interval_min
|
| 126 |
+
speed = path_length / time_tracked if time_tracked > 0 else 0
|
| 127 |
+
|
| 128 |
+
metrics.append(
|
| 129 |
+
{
|
| 130 |
+
"cell_id": cell_id,
|
| 131 |
+
"frames_tracked": len(cell_track),
|
| 132 |
+
"speed_um_per_min": speed,
|
| 133 |
+
"directionality": directionality,
|
| 134 |
+
"displacement_um": net_displacement,
|
| 135 |
+
"path_length_um": path_length,
|
| 136 |
+
}
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
# Convert metrics to DataFrame
|
| 140 |
+
metrics_df = pd.DataFrame(metrics)
|
| 141 |
+
|
| 142 |
+
# Save metrics to CSV
|
| 143 |
+
metrics_file = os.path.join(output_dir, "cell_migration_metrics.csv")
|
| 144 |
+
metrics_df.to_csv(metrics_file, index=False)
|
| 145 |
+
|
| 146 |
+
# Calculate summary statistics
|
| 147 |
+
summary = {
|
| 148 |
+
"num_cells_tracked": len(metrics_df),
|
| 149 |
+
"avg_speed": metrics_df["speed_um_per_min"].mean(),
|
| 150 |
+
"std_speed": metrics_df["speed_um_per_min"].std(),
|
| 151 |
+
"avg_directionality": metrics_df["directionality"].mean(),
|
| 152 |
+
"std_directionality": metrics_df["directionality"].std(),
|
| 153 |
+
"avg_displacement": metrics_df["displacement_um"].mean(),
|
| 154 |
+
"std_displacement": metrics_df["displacement_um"].std(),
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
# Save summary statistics
|
| 158 |
+
summary_file = os.path.join(output_dir, "migration_summary.csv")
|
| 159 |
+
pd.DataFrame([summary]).to_csv(summary_file, index=False)
|
| 160 |
+
|
| 161 |
+
# Save trajectories visualization
|
| 162 |
+
fig, ax = plt.figure(figsize=(8, 8)), plt.gca()
|
| 163 |
+
tp.plot_traj(trajectories, ax=ax)
|
| 164 |
+
plt.title("Cell Migration Trajectories")
|
| 165 |
+
plt.xlabel("x position (pixels)")
|
| 166 |
+
plt.ylabel("y position (pixels)")
|
| 167 |
+
|
| 168 |
+
trajectories_file = os.path.join(output_dir, "cell_trajectories.png")
|
| 169 |
+
plt.savefig(trajectories_file)
|
| 170 |
+
plt.close()
|
| 171 |
+
|
| 172 |
+
# Create a rose plot to show migration directionality
|
| 173 |
+
fig, ax = plt.subplots(subplot_kw={"projection": "polar"}, figsize=(8, 8))
|
| 174 |
+
|
| 175 |
+
# Calculate angles for each cell (end position relative to start)
|
| 176 |
+
angles = []
|
| 177 |
+
for cell_id in cell_ids:
|
| 178 |
+
cell_track = trajectories[trajectories["particle"] == cell_id].sort_values("frame")
|
| 179 |
+
start_x, start_y = cell_track.iloc[0][["x", "y"]]
|
| 180 |
+
end_x, end_y = cell_track.iloc[-1][["x", "y"]]
|
| 181 |
+
dx, dy = end_x - start_x, end_y - start_y
|
| 182 |
+
angle = np.arctan2(dy, dx)
|
| 183 |
+
angles.append(angle)
|
| 184 |
+
|
| 185 |
+
# Plot the histogram
|
| 186 |
+
bins = np.linspace(-np.pi, np.pi, 16)
|
| 187 |
+
ax.hist(angles, bins=bins)
|
| 188 |
+
ax.set_title("Cell Migration Directionality")
|
| 189 |
+
|
| 190 |
+
# Save the rose plot
|
| 191 |
+
rose_plot_file = os.path.join(output_dir, "rose_plot.png")
|
| 192 |
+
plt.savefig(rose_plot_file)
|
| 193 |
+
plt.close()
|
| 194 |
+
|
| 195 |
+
# Create a displacement plot
|
| 196 |
+
plt.figure(figsize=(10, 6))
|
| 197 |
+
plt.bar(range(len(metrics_df)), metrics_df["displacement_um"])
|
| 198 |
+
plt.xlabel("Cell ID")
|
| 199 |
+
plt.ylabel("Displacement (μm)")
|
| 200 |
+
plt.title("Cell Displacements")
|
| 201 |
+
|
| 202 |
+
# Save the displacement plot
|
| 203 |
+
displacement_plot_file = os.path.join(output_dir, "track_displacement_plot.png")
|
| 204 |
+
plt.savefig(displacement_plot_file)
|
| 205 |
+
plt.close()
|
| 206 |
+
|
| 207 |
+
# Generate research log
|
| 208 |
+
log = f"""
|
| 209 |
+
Cell Migration Analysis Research Log:
|
| 210 |
+
|
| 211 |
+
1. Analyzed time-lapse sequence with {len(frames)} frames
|
| 212 |
+
2. Detected and tracked {len(cell_ids)} cells that persisted for at least {min_track_length} frames
|
| 213 |
+
3. Calculated key migration metrics:
|
| 214 |
+
- Average speed: {summary["avg_speed"]:.2f} ± {summary["std_speed"]:.2f} μm/min
|
| 215 |
+
- Average directionality ratio: {summary["avg_directionality"]:.2f} ± {summary["std_directionality"]:.2f}
|
| 216 |
+
- Average displacement: {summary["avg_displacement"]:.2f} ± {summary["std_displacement"]:.2f} μm
|
| 217 |
+
|
| 218 |
+
4. Files saved:
|
| 219 |
+
- Raw cell detections: {raw_detections_file}
|
| 220 |
+
- All cell trajectories: {all_trajectories_file}
|
| 221 |
+
- Filtered trajectories: {filtered_trajectories_file}
|
| 222 |
+
- Detailed cell metrics: {metrics_file}
|
| 223 |
+
- Summary statistics: {summary_file}
|
| 224 |
+
- Cell trajectories visualization: {trajectories_file}
|
| 225 |
+
- Direction rose plot: {rose_plot_file}
|
| 226 |
+
- Cell displacement plot: {displacement_plot_file}
|
| 227 |
+
|
| 228 |
+
Note: Analysis used pixel size of {pixel_size_um} μm and time interval of {time_interval_min} min between frames.
|
| 229 |
+
"""
|
| 230 |
+
|
| 231 |
+
return log.strip()
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def perform_crispr_cas9_genome_editing(guide_rna_sequences, target_genomic_loci, cell_tissue_type):
|
| 235 |
+
"""Simulates CRISPR-Cas9 genome editing process including guide RNA design, delivery, and analysis.
|
| 236 |
+
|
| 237 |
+
Parameters
|
| 238 |
+
----------
|
| 239 |
+
guide_rna_sequences : list of str
|
| 240 |
+
List of guide RNA sequences (20 nucleotides each) targeting the genomic region of interest
|
| 241 |
+
|
| 242 |
+
target_genomic_loci : str
|
| 243 |
+
Target genomic sequence to be edited (should be longer than guide RNA and contain the target sites)
|
| 244 |
+
|
| 245 |
+
cell_tissue_type : str
|
| 246 |
+
Type of cell or tissue being edited (affects delivery efficiency and editing outcomes)
|
| 247 |
+
|
| 248 |
+
Returns
|
| 249 |
+
-------
|
| 250 |
+
str
|
| 251 |
+
Research log detailing the CRISPR-Cas9 editing process, including steps taken and results
|
| 252 |
+
|
| 253 |
+
"""
|
| 254 |
+
import os
|
| 255 |
+
import random
|
| 256 |
+
from datetime import datetime
|
| 257 |
+
|
| 258 |
+
# Initialize research log
|
| 259 |
+
log = "CRISPR-Cas9 Genome Editing Research Log\n"
|
| 260 |
+
log += f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
| 261 |
+
log += f"Cell/Tissue Type: {cell_tissue_type}\n\n"
|
| 262 |
+
|
| 263 |
+
# Step 1: Validate guide RNA sequences
|
| 264 |
+
log += "STEP 1: Guide RNA Validation\n"
|
| 265 |
+
valid_guides = []
|
| 266 |
+
|
| 267 |
+
for i, guide in enumerate(guide_rna_sequences):
|
| 268 |
+
if len(guide) != 20:
|
| 269 |
+
log += f" Guide {i + 1}: INVALID - Guide RNA must be 20 nucleotides (current length: {len(guide)})\n"
|
| 270 |
+
continue
|
| 271 |
+
|
| 272 |
+
if not all(n in "ATGC" for n in guide.upper()):
|
| 273 |
+
log += f" Guide {i + 1}: INVALID - Guide RNA contains invalid nucleotides\n"
|
| 274 |
+
continue
|
| 275 |
+
|
| 276 |
+
# Calculate GC content (affects guide efficiency)
|
| 277 |
+
gc_content = (guide.upper().count("G") + guide.upper().count("C")) / len(guide) * 100
|
| 278 |
+
efficiency_score = 0
|
| 279 |
+
|
| 280 |
+
if 40 <= gc_content <= 60:
|
| 281 |
+
efficiency_score += 1
|
| 282 |
+
gc_quality = "Optimal"
|
| 283 |
+
else:
|
| 284 |
+
gc_quality = "Suboptimal"
|
| 285 |
+
|
| 286 |
+
log += f" Guide {i + 1}: VALID - {guide} (GC content: {gc_content:.1f}% - {gc_quality})\n"
|
| 287 |
+
valid_guides.append((guide, efficiency_score))
|
| 288 |
+
|
| 289 |
+
if not valid_guides:
|
| 290 |
+
log += "\nNo valid guide RNAs found. Genome editing cannot proceed.\n"
|
| 291 |
+
return log
|
| 292 |
+
|
| 293 |
+
# Step 2: Target site identification
|
| 294 |
+
log += "\nSTEP 2: Target Site Identification\n"
|
| 295 |
+
|
| 296 |
+
target_seq = target_genomic_loci.upper()
|
| 297 |
+
target_matches = []
|
| 298 |
+
|
| 299 |
+
for i, (guide, score) in enumerate(valid_guides):
|
| 300 |
+
# Find guide RNA target in genomic sequence (including PAM site NGG)
|
| 301 |
+
guide.upper() + "NGG"
|
| 302 |
+
|
| 303 |
+
# Check if guide sequence is in target (simplified)
|
| 304 |
+
if guide.upper() in target_seq:
|
| 305 |
+
position = target_seq.find(guide.upper())
|
| 306 |
+
# Check if there's a PAM sequence (NGG) after the guide
|
| 307 |
+
if position + len(guide) + 2 <= len(target_seq):
|
| 308 |
+
potential_pam = target_seq[position + len(guide) : position + len(guide) + 3]
|
| 309 |
+
if potential_pam[1:3] == "GG":
|
| 310 |
+
pam_quality = "Found"
|
| 311 |
+
score += 2
|
| 312 |
+
else:
|
| 313 |
+
pam_quality = "Not found"
|
| 314 |
+
else:
|
| 315 |
+
pam_quality = "Out of bounds"
|
| 316 |
+
|
| 317 |
+
log += f" Guide {i + 1}: Found at position {position} (PAM: {pam_quality})\n"
|
| 318 |
+
target_matches.append((guide, position, score))
|
| 319 |
+
else:
|
| 320 |
+
log += f" Guide {i + 1}: No match found in target sequence\n"
|
| 321 |
+
|
| 322 |
+
if not target_matches:
|
| 323 |
+
log += "\nNo matching target sites found. Genome editing cannot proceed.\n"
|
| 324 |
+
return log
|
| 325 |
+
|
| 326 |
+
# Step 3: Simulate CRISPR-Cas9 delivery
|
| 327 |
+
log += "\nSTEP 3: CRISPR-Cas9 Delivery Simulation\n"
|
| 328 |
+
|
| 329 |
+
# Cell-specific delivery efficiencies (simplified model)
|
| 330 |
+
delivery_efficiencies = {
|
| 331 |
+
"hek293": 0.85,
|
| 332 |
+
"hela": 0.75,
|
| 333 |
+
"ipsc": 0.60,
|
| 334 |
+
"primary_neuron": 0.40,
|
| 335 |
+
"hematopoietic_stem_cell": 0.55,
|
| 336 |
+
"mouse_embryo": 0.70,
|
| 337 |
+
"plant_cell": 0.30,
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
# Get delivery efficiency based on cell type (default to 0.5 if unknown)
|
| 341 |
+
cell_type_key = cell_tissue_type.lower().replace(" ", "_")
|
| 342 |
+
delivery_efficiency = delivery_efficiencies.get(cell_type_key, 0.5)
|
| 343 |
+
|
| 344 |
+
log += f" Delivery method: Lipofection for {cell_tissue_type}\n"
|
| 345 |
+
log += f" Estimated delivery efficiency: {delivery_efficiency * 100:.1f}%\n"
|
| 346 |
+
|
| 347 |
+
# Step 4: Simulate genome editing
|
| 348 |
+
log += "\nSTEP 4: Genome Editing Simulation\n"
|
| 349 |
+
|
| 350 |
+
# Select best guide based on score
|
| 351 |
+
best_guide, best_position, best_score = sorted(target_matches, key=lambda x: x[2], reverse=True)[0]
|
| 352 |
+
|
| 353 |
+
log += f" Selected guide RNA: {best_guide} (highest efficiency score)\n"
|
| 354 |
+
log += f" Target position: {best_position} to {best_position + len(best_guide) - 1}\n"
|
| 355 |
+
|
| 356 |
+
# Simulate editing outcome
|
| 357 |
+
edit_success_rate = delivery_efficiency * (0.5 + (best_score * 0.1)) # Between 50-90% based on guide quality
|
| 358 |
+
|
| 359 |
+
# Cut site (typically 3 bases upstream of PAM)
|
| 360 |
+
cut_position = best_position + len(best_guide) - 3
|
| 361 |
+
log += f" Predicted cut site: Between positions {cut_position} and {cut_position + 1}\n"
|
| 362 |
+
|
| 363 |
+
# Simulate editing outcomes
|
| 364 |
+
indel_size = random.randint(1, 5) # Random indel size between 1-5 bp
|
| 365 |
+
|
| 366 |
+
# Create modified sequence (simulate a deletion for simplicity)
|
| 367 |
+
modified_sequence = target_seq[:cut_position] + target_seq[cut_position + indel_size :]
|
| 368 |
+
|
| 369 |
+
log += f" Simulated edit: {indel_size}bp deletion at cut site\n"
|
| 370 |
+
log += f" Predicted editing efficiency: {edit_success_rate * 100:.1f}%\n"
|
| 371 |
+
|
| 372 |
+
# Step 5: Analysis of editing outcomes
|
| 373 |
+
log += "\nSTEP 5: Editing Outcome Analysis\n"
|
| 374 |
+
|
| 375 |
+
# Calculate basic stats
|
| 376 |
+
log += f" Original sequence length: {len(target_seq)} bp\n"
|
| 377 |
+
log += f" Modified sequence length: {len(modified_sequence)} bp\n"
|
| 378 |
+
|
| 379 |
+
# Save sequences to files
|
| 380 |
+
os.makedirs("crispr_results", exist_ok=True)
|
| 381 |
+
|
| 382 |
+
original_file = "crispr_results/original_sequence.txt"
|
| 383 |
+
with open(original_file, "w") as f:
|
| 384 |
+
f.write(f">Original_Sequence\n{target_seq}\n")
|
| 385 |
+
|
| 386 |
+
modified_file = "crispr_results/modified_sequence.txt"
|
| 387 |
+
with open(modified_file, "w") as f:
|
| 388 |
+
f.write(f">Modified_Sequence\n{modified_sequence}\n")
|
| 389 |
+
|
| 390 |
+
log += f" Original sequence saved to: {original_file}\n"
|
| 391 |
+
log += f" Modified sequence saved to: {modified_file}\n"
|
| 392 |
+
|
| 393 |
+
# Summary
|
| 394 |
+
log += "\nSUMMARY:\n"
|
| 395 |
+
log += f" CRISPR-Cas9 editing successfully simulated for {cell_tissue_type}\n"
|
| 396 |
+
log += f" {indel_size}bp deletion introduced at position {cut_position}\n"
|
| 397 |
+
log += f" Expected success rate in cell population: {edit_success_rate * 100:.1f}%\n"
|
| 398 |
+
|
| 399 |
+
return log
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def analyze_calcium_imaging_data(image_stack_path, output_dir="./"):
|
| 403 |
+
"""Analyze calcium imaging data to quantify neuronal activity metrics.
|
| 404 |
+
|
| 405 |
+
This function processes fluorescence microscopy images of GCaMP-labeled neurons
|
| 406 |
+
to extract quantitative metrics of neuronal activity, including cell counts,
|
| 407 |
+
event rates, decay times, and signal-to-noise ratios.
|
| 408 |
+
|
| 409 |
+
Parameters
|
| 410 |
+
----------
|
| 411 |
+
image_stack_path : str
|
| 412 |
+
Path to the time-series stack of fluorescence microscopy images (TIFF format)
|
| 413 |
+
output_dir : str, optional
|
| 414 |
+
Directory to save output files (default: "./")
|
| 415 |
+
|
| 416 |
+
Returns
|
| 417 |
+
-------
|
| 418 |
+
str
|
| 419 |
+
Research log summarizing the analysis steps and results
|
| 420 |
+
|
| 421 |
+
"""
|
| 422 |
+
import os
|
| 423 |
+
|
| 424 |
+
import numpy as np
|
| 425 |
+
import pandas as pd
|
| 426 |
+
from scipy import ndimage, signal
|
| 427 |
+
from scipy.optimize import curve_fit
|
| 428 |
+
from skimage import feature, filters, io, measure, segmentation
|
| 429 |
+
|
| 430 |
+
# Create output directory if it doesn't exist
|
| 431 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 432 |
+
|
| 433 |
+
# Step 1: Load the image stack
|
| 434 |
+
log = "CALCIUM IMAGING ANALYSIS LOG\n"
|
| 435 |
+
log += "===========================\n\n"
|
| 436 |
+
log += f"Loading image stack from: {image_stack_path}\n"
|
| 437 |
+
|
| 438 |
+
try:
|
| 439 |
+
image_stack = io.imread(image_stack_path)
|
| 440 |
+
num_frames, height, width = image_stack.shape
|
| 441 |
+
log += f"Successfully loaded {num_frames} frames of size {height}x{width}\n\n"
|
| 442 |
+
except Exception as e:
|
| 443 |
+
return f"Error loading image stack: {str(e)}"
|
| 444 |
+
|
| 445 |
+
# Step 2: Calculate mean image for segmentation
|
| 446 |
+
log += "Step 1: Preprocessing and neuron segmentation\n"
|
| 447 |
+
mean_image = np.mean(image_stack, axis=0)
|
| 448 |
+
|
| 449 |
+
# Apply Gaussian filter to reduce noise
|
| 450 |
+
smooth_mean = filters.gaussian(mean_image, sigma=2)
|
| 451 |
+
|
| 452 |
+
# Step 3: Segment neurons using watershed
|
| 453 |
+
# Find local maxima (potential cell centers)
|
| 454 |
+
distance = ndimage.distance_transform_edt(smooth_mean)
|
| 455 |
+
# Create a mask for local maxima instead of using indices=False parameter
|
| 456 |
+
coordinates = feature.peak_local_max(distance, min_distance=10)
|
| 457 |
+
|
| 458 |
+
# Handle case where no local maxima are found
|
| 459 |
+
if len(coordinates) == 0:
|
| 460 |
+
log += "No local maxima detected. Using simple thresholding instead.\n"
|
| 461 |
+
# Create a simple binary mask using thresholding as fallback
|
| 462 |
+
binary_mask = smooth_mean > filters.threshold_otsu(smooth_mean)
|
| 463 |
+
markers = measure.label(binary_mask)
|
| 464 |
+
else:
|
| 465 |
+
local_max = np.zeros_like(distance, dtype=bool)
|
| 466 |
+
for coord in coordinates:
|
| 467 |
+
local_max[coord[0], coord[1]] = True
|
| 468 |
+
markers = measure.label(local_max)
|
| 469 |
+
|
| 470 |
+
# Watershed segmentation
|
| 471 |
+
segmented = segmentation.watershed(-smooth_mean, markers, mask=smooth_mean > filters.threshold_otsu(smooth_mean))
|
| 472 |
+
|
| 473 |
+
# Get region properties
|
| 474 |
+
regions = measure.regionprops(segmented)
|
| 475 |
+
cell_count = len(regions)
|
| 476 |
+
log += f"Detected {cell_count} neurons in the field of view\n\n"
|
| 477 |
+
|
| 478 |
+
# Step 4: Extract time-series data for each neuron
|
| 479 |
+
log += "Step 2: Extracting fluorescence time-series for each neuron\n"
|
| 480 |
+
time_series_data = []
|
| 481 |
+
|
| 482 |
+
for _i, region in enumerate(regions):
|
| 483 |
+
mask = segmented == region.label
|
| 484 |
+
cell_time_series = []
|
| 485 |
+
|
| 486 |
+
for frame in range(num_frames):
|
| 487 |
+
intensity = np.mean(image_stack[frame][mask])
|
| 488 |
+
cell_time_series.append(intensity)
|
| 489 |
+
|
| 490 |
+
time_series_data.append(cell_time_series)
|
| 491 |
+
|
| 492 |
+
time_series_array = np.array(time_series_data)
|
| 493 |
+
|
| 494 |
+
# Step 5: Detect calcium events and calculate metrics
|
| 495 |
+
log += "Step 3: Calculating neuronal activity metrics\n"
|
| 496 |
+
|
| 497 |
+
# Function to fit exponential decay
|
| 498 |
+
def exp_decay(x, a, tau, c):
|
| 499 |
+
return a * np.exp(-x / tau) + c
|
| 500 |
+
|
| 501 |
+
event_rates = []
|
| 502 |
+
decay_times = []
|
| 503 |
+
snr_values = []
|
| 504 |
+
|
| 505 |
+
for i, ts in enumerate(time_series_data):
|
| 506 |
+
# Normalize time series
|
| 507 |
+
baseline = np.percentile(ts, 20)
|
| 508 |
+
ts_norm = [(x - baseline) / baseline for x in ts]
|
| 509 |
+
|
| 510 |
+
# Simple event detection (threshold crossing)
|
| 511 |
+
threshold = np.std(ts_norm) * 2
|
| 512 |
+
events = []
|
| 513 |
+
in_event = False
|
| 514 |
+
|
| 515 |
+
for j, val in enumerate(ts_norm):
|
| 516 |
+
if not in_event and val > threshold:
|
| 517 |
+
events.append(j)
|
| 518 |
+
in_event = True
|
| 519 |
+
elif in_event and val < threshold:
|
| 520 |
+
in_event = False
|
| 521 |
+
|
| 522 |
+
# Calculate event rate (events per minute, assuming 10 Hz acquisition)
|
| 523 |
+
acquisition_rate = 10 # Hz (assumption)
|
| 524 |
+
recording_time_minutes = num_frames / acquisition_rate / 60
|
| 525 |
+
event_rate = len(events) / recording_time_minutes
|
| 526 |
+
event_rates.append(event_rate)
|
| 527 |
+
|
| 528 |
+
# Calculate decay times for detected events
|
| 529 |
+
cell_decay_times = []
|
| 530 |
+
for event_start in events:
|
| 531 |
+
if event_start + 30 < len(ts_norm): # Ensure enough frames after event
|
| 532 |
+
event_window = ts_norm[event_start : event_start + 30]
|
| 533 |
+
peak_idx = np.argmax(event_window)
|
| 534 |
+
decay_segment = event_window[peak_idx:]
|
| 535 |
+
|
| 536 |
+
try:
|
| 537 |
+
# Fit exponential decay
|
| 538 |
+
x_data = np.arange(len(decay_segment))
|
| 539 |
+
popt, _ = curve_fit(
|
| 540 |
+
exp_decay,
|
| 541 |
+
x_data,
|
| 542 |
+
decay_segment,
|
| 543 |
+
p0=[decay_segment[0], 5, decay_segment[-1]],
|
| 544 |
+
bounds=([0, 0, 0], [np.inf, np.inf, np.inf]),
|
| 545 |
+
)
|
| 546 |
+
tau = popt[1] # Decay time constant
|
| 547 |
+
cell_decay_times.append(tau / acquisition_rate) # Convert to seconds
|
| 548 |
+
except Exception:
|
| 549 |
+
# Skip if curve fitting fails
|
| 550 |
+
pass
|
| 551 |
+
|
| 552 |
+
if cell_decay_times:
|
| 553 |
+
decay_times.append(np.mean(cell_decay_times))
|
| 554 |
+
else:
|
| 555 |
+
decay_times.append(np.nan)
|
| 556 |
+
|
| 557 |
+
# Calculate signal-to-noise ratio
|
| 558 |
+
signal = np.mean([ts_norm[e] for e in events]) if events else 0 # noqa: F811
|
| 559 |
+
noise = np.std([ts_norm[i] for i in range(len(ts_norm)) if all(abs(i - e) > 5 for e in events)])
|
| 560 |
+
snr = signal / noise if noise > 0 else 0
|
| 561 |
+
snr_values.append(snr)
|
| 562 |
+
|
| 563 |
+
# Step 6: Compile and save results
|
| 564 |
+
cell_metrics = pd.DataFrame(
|
| 565 |
+
{
|
| 566 |
+
"Cell_ID": range(1, cell_count + 1),
|
| 567 |
+
"Event_Rate_per_min": event_rates,
|
| 568 |
+
"Decay_Time_sec": decay_times,
|
| 569 |
+
"SNR": snr_values,
|
| 570 |
+
}
|
| 571 |
+
)
|
| 572 |
+
|
| 573 |
+
metrics_file = os.path.join(output_dir, "neuron_activity_metrics.csv")
|
| 574 |
+
cell_metrics.to_csv(metrics_file, index=False)
|
| 575 |
+
|
| 576 |
+
# Save time series data
|
| 577 |
+
time_series_file = os.path.join(output_dir, "neuron_time_series.csv")
|
| 578 |
+
time_series_df = pd.DataFrame(time_series_array.T)
|
| 579 |
+
time_series_df.columns = [f"Cell_{i + 1}" for i in range(cell_count)]
|
| 580 |
+
time_series_df.to_csv(time_series_file, index=False)
|
| 581 |
+
|
| 582 |
+
# Step 7: Summarize results
|
| 583 |
+
log += f"Cell count: {cell_count}\n"
|
| 584 |
+
log += f"Average event rate: {np.nanmean(event_rates):.2f} events/min\n"
|
| 585 |
+
log += f"Average decay time: {np.nanmean(decay_times):.2f} seconds\n"
|
| 586 |
+
log += f"Average SNR: {np.nanmean(snr_values):.2f}\n\n"
|
| 587 |
+
|
| 588 |
+
log += "Step 4: Results saved to files\n"
|
| 589 |
+
log += f"Neuron activity metrics saved to: {metrics_file}\n"
|
| 590 |
+
log += f"Time series data saved to: {time_series_file}\n"
|
| 591 |
+
|
| 592 |
+
return log
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
def analyze_in_vitro_drug_release_kinetics(
|
| 596 |
+
time_points,
|
| 597 |
+
concentration_data,
|
| 598 |
+
drug_name="Drug",
|
| 599 |
+
total_drug_loaded=None,
|
| 600 |
+
output_dir="./",
|
| 601 |
+
):
|
| 602 |
+
"""Analyzes in vitro drug release kinetics from biomaterial formulations.
|
| 603 |
+
|
| 604 |
+
Parameters
|
| 605 |
+
----------
|
| 606 |
+
time_points : list or numpy.ndarray
|
| 607 |
+
Time points at which drug concentrations were measured (in hours)
|
| 608 |
+
concentration_data : list or numpy.ndarray
|
| 609 |
+
Measured drug concentration at each time point
|
| 610 |
+
drug_name : str, optional
|
| 611 |
+
Name of the drug being analyzed (default: "Drug")
|
| 612 |
+
total_drug_loaded : float, optional
|
| 613 |
+
Total amount of drug initially loaded in the formulation.
|
| 614 |
+
If None, the maximum concentration is used as 100% (default: None)
|
| 615 |
+
output_dir : str, optional
|
| 616 |
+
Directory to save output files (default: "./")
|
| 617 |
+
|
| 618 |
+
Returns
|
| 619 |
+
-------
|
| 620 |
+
str
|
| 621 |
+
Research log summarizing the analysis steps, results, and saved file locations
|
| 622 |
+
|
| 623 |
+
"""
|
| 624 |
+
import os
|
| 625 |
+
from datetime import datetime
|
| 626 |
+
|
| 627 |
+
import matplotlib.pyplot as plt
|
| 628 |
+
import numpy as np
|
| 629 |
+
import pandas as pd
|
| 630 |
+
from scipy.optimize import curve_fit
|
| 631 |
+
|
| 632 |
+
# Ensure output directory exists
|
| 633 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 634 |
+
|
| 635 |
+
# Convert inputs to numpy arrays
|
| 636 |
+
time_points = np.array(time_points)
|
| 637 |
+
concentration_data = np.array(concentration_data)
|
| 638 |
+
|
| 639 |
+
# Calculate cumulative release percentage
|
| 640 |
+
if total_drug_loaded is None:
|
| 641 |
+
total_drug_loaded = np.max(concentration_data)
|
| 642 |
+
|
| 643 |
+
cumulative_release = (concentration_data / total_drug_loaded) * 100
|
| 644 |
+
|
| 645 |
+
# Create a DataFrame for easier manipulation
|
| 646 |
+
release_df = pd.DataFrame(
|
| 647 |
+
{
|
| 648 |
+
"Time (hours)": time_points,
|
| 649 |
+
"Concentration": concentration_data,
|
| 650 |
+
"Cumulative Release (%)": cumulative_release,
|
| 651 |
+
}
|
| 652 |
+
)
|
| 653 |
+
|
| 654 |
+
# Calculate release rate (simple approximation using differences)
|
| 655 |
+
release_df["Release Rate"] = np.gradient(release_df["Cumulative Release (%)"], release_df["Time (hours)"])
|
| 656 |
+
|
| 657 |
+
# Define kinetic models
|
| 658 |
+
def zero_order(t, k):
|
| 659 |
+
return k * t
|
| 660 |
+
|
| 661 |
+
def first_order(t, k):
|
| 662 |
+
return 100 * (1 - np.exp(-k * t))
|
| 663 |
+
|
| 664 |
+
def higuchi(t, k):
|
| 665 |
+
return k * np.sqrt(t)
|
| 666 |
+
|
| 667 |
+
def korsmeyer_peppas(t, k, n):
|
| 668 |
+
return 100 * (k * t) ** n
|
| 669 |
+
|
| 670 |
+
# Fit data to different kinetic models
|
| 671 |
+
models = {}
|
| 672 |
+
r2_values = {}
|
| 673 |
+
|
| 674 |
+
# Zero-order kinetics
|
| 675 |
+
try:
|
| 676 |
+
params, _ = curve_fit(zero_order, time_points, cumulative_release)
|
| 677 |
+
y_pred = zero_order(time_points, *params)
|
| 678 |
+
ss_total = np.sum((cumulative_release - np.mean(cumulative_release)) ** 2)
|
| 679 |
+
ss_residual = np.sum((cumulative_release - y_pred) ** 2)
|
| 680 |
+
r2 = 1 - (ss_residual / ss_total)
|
| 681 |
+
models["Zero-order"] = {
|
| 682 |
+
"params": params,
|
| 683 |
+
"equation": f"Release = {params[0]:.4f} * t",
|
| 684 |
+
"pred": y_pred,
|
| 685 |
+
}
|
| 686 |
+
r2_values["Zero-order"] = r2
|
| 687 |
+
except Exception:
|
| 688 |
+
models["Zero-order"] = {
|
| 689 |
+
"params": None,
|
| 690 |
+
"equation": "Fitting failed",
|
| 691 |
+
"pred": None,
|
| 692 |
+
}
|
| 693 |
+
r2_values["Zero-order"] = 0
|
| 694 |
+
|
| 695 |
+
# First-order kinetics
|
| 696 |
+
try:
|
| 697 |
+
params, _ = curve_fit(first_order, time_points, cumulative_release, bounds=(0, [1]))
|
| 698 |
+
y_pred = first_order(time_points, *params)
|
| 699 |
+
ss_total = np.sum((cumulative_release - np.mean(cumulative_release)) ** 2)
|
| 700 |
+
ss_residual = np.sum((cumulative_release - y_pred) ** 2)
|
| 701 |
+
r2 = 1 - (ss_residual / ss_total)
|
| 702 |
+
models["First-order"] = {
|
| 703 |
+
"params": params,
|
| 704 |
+
"equation": f"Release = 100 * (1 - exp(-{params[0]:.4f} * t))",
|
| 705 |
+
"pred": y_pred,
|
| 706 |
+
}
|
| 707 |
+
r2_values["First-order"] = r2
|
| 708 |
+
except Exception:
|
| 709 |
+
models["First-order"] = {
|
| 710 |
+
"params": None,
|
| 711 |
+
"equation": "Fitting failed",
|
| 712 |
+
"pred": None,
|
| 713 |
+
}
|
| 714 |
+
r2_values["First-order"] = 0
|
| 715 |
+
|
| 716 |
+
# Higuchi model
|
| 717 |
+
try:
|
| 718 |
+
params, _ = curve_fit(higuchi, time_points, cumulative_release)
|
| 719 |
+
y_pred = higuchi(time_points, *params)
|
| 720 |
+
ss_total = np.sum((cumulative_release - np.mean(cumulative_release)) ** 2)
|
| 721 |
+
ss_residual = np.sum((cumulative_release - y_pred) ** 2)
|
| 722 |
+
r2 = 1 - (ss_residual / ss_total)
|
| 723 |
+
models["Higuchi"] = {
|
| 724 |
+
"params": params,
|
| 725 |
+
"equation": f"Release = {params[0]:.4f} * sqrt(t)",
|
| 726 |
+
"pred": y_pred,
|
| 727 |
+
}
|
| 728 |
+
r2_values["Higuchi"] = r2
|
| 729 |
+
except Exception:
|
| 730 |
+
models["Higuchi"] = {"params": None, "equation": "Fitting failed", "pred": None}
|
| 731 |
+
r2_values["Higuchi"] = 0
|
| 732 |
+
|
| 733 |
+
# Korsmeyer-Peppas model
|
| 734 |
+
try:
|
| 735 |
+
# Only use the first 60% of release data for Korsmeyer-Peppas model
|
| 736 |
+
mask = cumulative_release <= 60
|
| 737 |
+
if sum(mask) >= 3: # Need at least 3 points for fitting
|
| 738 |
+
params, _ = curve_fit(
|
| 739 |
+
korsmeyer_peppas,
|
| 740 |
+
time_points[mask],
|
| 741 |
+
cumulative_release[mask],
|
| 742 |
+
bounds=([0, 0], [1, 1]),
|
| 743 |
+
)
|
| 744 |
+
y_pred = korsmeyer_peppas(time_points, *params)
|
| 745 |
+
ss_total = np.sum((cumulative_release - np.mean(cumulative_release)) ** 2)
|
| 746 |
+
ss_residual = np.sum((cumulative_release - y_pred) ** 2)
|
| 747 |
+
r2 = 1 - (ss_residual / ss_total)
|
| 748 |
+
models["Korsmeyer-Peppas"] = {
|
| 749 |
+
"params": params,
|
| 750 |
+
"equation": f"Release = 100 * ({params[0]:.4f} * t)^{params[1]:.4f}",
|
| 751 |
+
"pred": y_pred,
|
| 752 |
+
}
|
| 753 |
+
r2_values["Korsmeyer-Peppas"] = r2
|
| 754 |
+
else:
|
| 755 |
+
models["Korsmeyer-Peppas"] = {
|
| 756 |
+
"params": None,
|
| 757 |
+
"equation": "Insufficient data points",
|
| 758 |
+
"pred": None,
|
| 759 |
+
}
|
| 760 |
+
r2_values["Korsmeyer-Peppas"] = 0
|
| 761 |
+
except Exception:
|
| 762 |
+
models["Korsmeyer-Peppas"] = {
|
| 763 |
+
"params": None,
|
| 764 |
+
"equation": "Fitting failed",
|
| 765 |
+
"pred": None,
|
| 766 |
+
}
|
| 767 |
+
r2_values["Korsmeyer-Peppas"] = 0
|
| 768 |
+
|
| 769 |
+
# Determine best model based on R² value
|
| 770 |
+
best_model = max(r2_values, key=r2_values.get)
|
| 771 |
+
|
| 772 |
+
# Calculate half-life (time to 50% release)
|
| 773 |
+
try:
|
| 774 |
+
# Use best model to calculate half-life
|
| 775 |
+
if best_model == "Zero-order":
|
| 776 |
+
k = models[best_model]["params"][0]
|
| 777 |
+
half_life = 50 / k if k > 0 else float("inf")
|
| 778 |
+
elif best_model == "First-order":
|
| 779 |
+
k = models[best_model]["params"][0]
|
| 780 |
+
half_life = -np.log(0.5) / k if k > 0 else float("inf")
|
| 781 |
+
elif best_model == "Higuchi":
|
| 782 |
+
k = models[best_model]["params"][0]
|
| 783 |
+
half_life = (50 / k) ** 2 if k > 0 else float("inf")
|
| 784 |
+
elif best_model == "Korsmeyer-Peppas":
|
| 785 |
+
k, n = models[best_model]["params"]
|
| 786 |
+
half_life = (0.5 ** (1 / n)) / k if k > 0 else float("inf")
|
| 787 |
+
else:
|
| 788 |
+
# Interpolate from data if model fitting failed
|
| 789 |
+
from scipy.interpolate import interp1d
|
| 790 |
+
|
| 791 |
+
if np.max(cumulative_release) >= 50:
|
| 792 |
+
f = interp1d(cumulative_release, time_points)
|
| 793 |
+
half_life = float(f(50))
|
| 794 |
+
else:
|
| 795 |
+
half_life = "Not reached"
|
| 796 |
+
except Exception:
|
| 797 |
+
half_life = "Could not calculate"
|
| 798 |
+
|
| 799 |
+
# Create plots
|
| 800 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 801 |
+
|
| 802 |
+
# 1. Cumulative release plot with model fits
|
| 803 |
+
plt.figure(figsize=(10, 6))
|
| 804 |
+
plt.plot(time_points, cumulative_release, "o-", label="Experimental data")
|
| 805 |
+
|
| 806 |
+
for model_name, model_data in models.items():
|
| 807 |
+
if model_data["pred"] is not None:
|
| 808 |
+
plt.plot(
|
| 809 |
+
time_points,
|
| 810 |
+
model_data["pred"],
|
| 811 |
+
"--",
|
| 812 |
+
label=f"{model_name} (R² = {r2_values[model_name]:.4f})",
|
| 813 |
+
)
|
| 814 |
+
|
| 815 |
+
plt.xlabel("Time (hours)")
|
| 816 |
+
plt.ylabel("Cumulative Release (%)")
|
| 817 |
+
plt.title(f"In Vitro Release Profile of {drug_name}")
|
| 818 |
+
plt.legend()
|
| 819 |
+
plt.grid(True, linestyle="--", alpha=0.7)
|
| 820 |
+
cumulative_plot_path = os.path.join(output_dir, f"cumulative_release_{timestamp}.png")
|
| 821 |
+
plt.savefig(cumulative_plot_path, dpi=300, bbox_inches="tight")
|
| 822 |
+
plt.close()
|
| 823 |
+
|
| 824 |
+
# 2. Release rate plot
|
| 825 |
+
plt.figure(figsize=(10, 6))
|
| 826 |
+
plt.plot(time_points, release_df["Release Rate"], "o-")
|
| 827 |
+
plt.xlabel("Time (hours)")
|
| 828 |
+
plt.ylabel("Release Rate (%/hour)")
|
| 829 |
+
plt.title(f"Release Rate of {drug_name}")
|
| 830 |
+
plt.grid(True, linestyle="--", alpha=0.7)
|
| 831 |
+
rate_plot_path = os.path.join(output_dir, f"release_rate_{timestamp}.png")
|
| 832 |
+
plt.savefig(rate_plot_path, dpi=300, bbox_inches="tight")
|
| 833 |
+
plt.close()
|
| 834 |
+
|
| 835 |
+
# Save data to CSV
|
| 836 |
+
csv_path = os.path.join(output_dir, f"drug_release_data_{timestamp}.csv")
|
| 837 |
+
release_df.to_csv(csv_path, index=False)
|
| 838 |
+
|
| 839 |
+
# Generate research log
|
| 840 |
+
log = f"""
|
| 841 |
+
# In Vitro Drug Release Kinetics Analysis for {drug_name}
|
| 842 |
+
|
| 843 |
+
## Analysis Summary
|
| 844 |
+
- **Date/Time:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 845 |
+
- **Drug Analyzed:** {drug_name}
|
| 846 |
+
- **Time Range:** {min(time_points)} to {max(time_points)} hours
|
| 847 |
+
- **Number of Data Points:** {len(time_points)}
|
| 848 |
+
- **Maximum Release Achieved:** {max(cumulative_release):.2f}%
|
| 849 |
+
|
| 850 |
+
## Kinetic Models Analysis
|
| 851 |
+
The release data was fitted to four standard kinetic models:
|
| 852 |
+
|
| 853 |
+
1. **Zero-order Model:** {models["Zero-order"]["equation"]} (R² = {r2_values["Zero-order"]:.4f})
|
| 854 |
+
2. **First-order Model:** {models["First-order"]["equation"]} (R² = {r2_values["First-order"]:.4f})
|
| 855 |
+
3. **Higuchi Model:** {models["Higuchi"]["equation"]} (R² = {r2_values["Higuchi"]:.4f})
|
| 856 |
+
4. **Korsmeyer-Peppas Model:** {models["Korsmeyer-Peppas"]["equation"]} (R² = {r2_values["Korsmeyer-Peppas"]:.4f})
|
| 857 |
+
|
| 858 |
+
**Best-fitting Model:** {best_model} (R² = {r2_values[best_model]:.4f})
|
| 859 |
+
|
| 860 |
+
## Release Metrics
|
| 861 |
+
- **Half-life (t50%):** {half_life if isinstance(half_life, str) else f"{half_life:.2f} hours"}
|
| 862 |
+
- **Initial Release Rate:** {release_df["Release Rate"].iloc[0]:.4f} %/hour
|
| 863 |
+
- **Average Release Rate:** {np.mean(release_df["Release Rate"]):.4f} %/hour
|
| 864 |
+
|
| 865 |
+
## Files Generated
|
| 866 |
+
1. Cumulative Release Plot: {cumulative_plot_path}
|
| 867 |
+
2. Release Rate Plot: {rate_plot_path}
|
| 868 |
+
3. Data CSV: {csv_path}
|
| 869 |
+
|
| 870 |
+
## Interpretation
|
| 871 |
+
The drug release profile of {drug_name} best follows a {
|
| 872 |
+
best_model
|
| 873 |
+
} kinetic model, which suggests that the release mechanism is primarily driven by {
|
| 874 |
+
"diffusion through a porous matrix"
|
| 875 |
+
if best_model == "Higuchi"
|
| 876 |
+
else "diffusion with erosion"
|
| 877 |
+
if best_model == "Korsmeyer-Peppas" and 0.43 <= models[best_model]["params"][1] <= 0.85
|
| 878 |
+
else "Fickian diffusion"
|
| 879 |
+
if best_model == "Korsmeyer-Peppas" and models[best_model]["params"][1] < 0.43
|
| 880 |
+
else "case-II transport"
|
| 881 |
+
if best_model == "Korsmeyer-Peppas" and models[best_model]["params"][1] > 0.85
|
| 882 |
+
else "concentration-dependent diffusion"
|
| 883 |
+
if best_model == "First-order"
|
| 884 |
+
else "constant release rate independent of concentration"
|
| 885 |
+
if best_model == "Zero-order"
|
| 886 |
+
else "complex mechanisms"
|
| 887 |
+
}.
|
| 888 |
+
"""
|
| 889 |
+
|
| 890 |
+
return log.strip()
|
| 891 |
+
|
| 892 |
+
|
| 893 |
+
def analyze_myofiber_morphology(
|
| 894 |
+
image_path,
|
| 895 |
+
nuclei_channel=2,
|
| 896 |
+
myofiber_channel=1,
|
| 897 |
+
threshold_method="otsu",
|
| 898 |
+
output_dir="./",
|
| 899 |
+
):
|
| 900 |
+
"""Quantifies morphological properties of myofibers in microscopy images of tissue sections.
|
| 901 |
+
|
| 902 |
+
Parameters
|
| 903 |
+
----------
|
| 904 |
+
image_path : str
|
| 905 |
+
Path to the microscopy image file (typically a multichannel image with nuclei and myofiber staining)
|
| 906 |
+
nuclei_channel : int, default=2
|
| 907 |
+
Channel index containing nuclei staining (DAPI, Hoechst, etc.)
|
| 908 |
+
myofiber_channel : int, default=1
|
| 909 |
+
Channel index containing myofiber staining (α-Actinin, etc.)
|
| 910 |
+
threshold_method : str, default='otsu'
|
| 911 |
+
Method for thresholding ('otsu', 'adaptive', or 'manual')
|
| 912 |
+
output_dir : str, default='./'
|
| 913 |
+
Directory to save output files
|
| 914 |
+
|
| 915 |
+
Returns
|
| 916 |
+
-------
|
| 917 |
+
str
|
| 918 |
+
Research log summarizing the analysis steps and results
|
| 919 |
+
|
| 920 |
+
"""
|
| 921 |
+
import os
|
| 922 |
+
from datetime import datetime
|
| 923 |
+
|
| 924 |
+
import numpy as np
|
| 925 |
+
import pandas as pd
|
| 926 |
+
from skimage import exposure, filters, io, measure, morphology
|
| 927 |
+
from skimage.color import label2rgb
|
| 928 |
+
|
| 929 |
+
# Create output directory if it doesn't exist
|
| 930 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 931 |
+
|
| 932 |
+
# Load the image
|
| 933 |
+
image = io.imread(image_path)
|
| 934 |
+
|
| 935 |
+
# Extract channels (assuming multichannel image)
|
| 936 |
+
if len(image.shape) > 2:
|
| 937 |
+
if len(image.shape) == 3:
|
| 938 |
+
# RGB image
|
| 939 |
+
nuclei_img = image[:, :, nuclei_channel] if nuclei_channel < image.shape[2] else image[:, :, 0]
|
| 940 |
+
myofiber_img = image[:, :, myofiber_channel] if myofiber_channel < image.shape[2] else image[:, :, 1]
|
| 941 |
+
else:
|
| 942 |
+
# Multichannel image (e.g., from confocal)
|
| 943 |
+
nuclei_img = image[nuclei_channel, :, :] if nuclei_channel < image.shape[0] else image[0, :, :]
|
| 944 |
+
myofiber_img = image[myofiber_channel, :, :] if myofiber_channel < image.shape[0] else image[1, :, :]
|
| 945 |
+
else:
|
| 946 |
+
# Single channel image - can't separate nuclei and myofibers
|
| 947 |
+
return "Error: Input image must be multichannel to separate nuclei and myofibers"
|
| 948 |
+
|
| 949 |
+
# Enhance contrast
|
| 950 |
+
nuclei_img = exposure.equalize_adapthist(nuclei_img)
|
| 951 |
+
myofiber_img = exposure.equalize_adapthist(myofiber_img)
|
| 952 |
+
|
| 953 |
+
# Segment nuclei
|
| 954 |
+
if threshold_method == "otsu":
|
| 955 |
+
nuclei_thresh = filters.threshold_otsu(nuclei_img)
|
| 956 |
+
elif threshold_method == "adaptive":
|
| 957 |
+
nuclei_thresh = filters.threshold_local(nuclei_img, block_size=35)
|
| 958 |
+
else: # manual
|
| 959 |
+
nuclei_thresh = np.mean(nuclei_img) * 1.5
|
| 960 |
+
|
| 961 |
+
nuclei_binary = nuclei_img > nuclei_thresh
|
| 962 |
+
nuclei_binary = morphology.remove_small_objects(nuclei_binary, min_size=30)
|
| 963 |
+
nuclei_binary = morphology.binary_closing(nuclei_binary)
|
| 964 |
+
|
| 965 |
+
# Label nuclei
|
| 966 |
+
nuclei_labels = measure.label(nuclei_binary)
|
| 967 |
+
nuclei_props = measure.regionprops(nuclei_labels)
|
| 968 |
+
|
| 969 |
+
# Segment myofibers
|
| 970 |
+
if threshold_method == "otsu":
|
| 971 |
+
myofiber_thresh = filters.threshold_otsu(myofiber_img)
|
| 972 |
+
elif threshold_method == "adaptive":
|
| 973 |
+
myofiber_thresh = filters.threshold_local(myofiber_img, block_size=101)
|
| 974 |
+
else: # manual
|
| 975 |
+
myofiber_thresh = np.mean(myofiber_img) * 1.2
|
| 976 |
+
|
| 977 |
+
myofiber_binary = myofiber_img > myofiber_thresh
|
| 978 |
+
myofiber_binary = morphology.remove_small_objects(myofiber_binary, min_size=500)
|
| 979 |
+
myofiber_binary = morphology.binary_closing(myofiber_binary, morphology.disk(3))
|
| 980 |
+
|
| 981 |
+
# Label myofibers
|
| 982 |
+
myofiber_labels = measure.label(myofiber_binary)
|
| 983 |
+
myofiber_props = measure.regionprops(myofiber_labels)
|
| 984 |
+
|
| 985 |
+
# Count nuclei inside myofibers
|
| 986 |
+
nuclei_inside = 0
|
| 987 |
+
nuclei_total = len(nuclei_props)
|
| 988 |
+
|
| 989 |
+
for nucleus in nuclei_props:
|
| 990 |
+
y, x = nucleus.centroid
|
| 991 |
+
y, x = int(y), int(x)
|
| 992 |
+
if myofiber_binary[y, x]:
|
| 993 |
+
nuclei_inside += 1
|
| 994 |
+
|
| 995 |
+
percent_inside = nuclei_inside / nuclei_total * 100 if nuclei_total > 0 else 0
|
| 996 |
+
|
| 997 |
+
# Calculate myofiber morphological properties
|
| 998 |
+
myofiber_data = []
|
| 999 |
+
for fiber in myofiber_props:
|
| 1000 |
+
myofiber_data.append(
|
| 1001 |
+
{
|
| 1002 |
+
"Area": fiber.area,
|
| 1003 |
+
"Perimeter": fiber.perimeter,
|
| 1004 |
+
"Eccentricity": fiber.eccentricity,
|
| 1005 |
+
"Solidity": fiber.solidity,
|
| 1006 |
+
"Orientation": fiber.orientation,
|
| 1007 |
+
}
|
| 1008 |
+
)
|
| 1009 |
+
|
| 1010 |
+
# Save results
|
| 1011 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1012 |
+
results_file = f"{output_dir}/myofiber_analysis_{timestamp}.csv"
|
| 1013 |
+
|
| 1014 |
+
if myofiber_data:
|
| 1015 |
+
df = pd.DataFrame(myofiber_data)
|
| 1016 |
+
df.to_csv(results_file, index=False)
|
| 1017 |
+
|
| 1018 |
+
# Calculate summary statistics
|
| 1019 |
+
mean_area = df["Area"].mean()
|
| 1020 |
+
mean_perimeter = df["Perimeter"].mean()
|
| 1021 |
+
mean_eccentricity = df["Eccentricity"].mean()
|
| 1022 |
+
else:
|
| 1023 |
+
mean_area = mean_perimeter = mean_eccentricity = 0
|
| 1024 |
+
|
| 1025 |
+
# Save labeled image
|
| 1026 |
+
labeled_image = label2rgb(myofiber_labels, image=myofiber_img)
|
| 1027 |
+
labeled_image_path = f"{output_dir}/labeled_myofibers_{timestamp}.png"
|
| 1028 |
+
io.imsave(labeled_image_path, (labeled_image * 255).astype(np.uint8))
|
| 1029 |
+
|
| 1030 |
+
# Create research log
|
| 1031 |
+
log = f"""
|
| 1032 |
+
MYOFIBER MORPHOLOGICAL ANALYSIS REPORT
|
| 1033 |
+
======================================
|
| 1034 |
+
Date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 1035 |
+
Image: {image_path}
|
| 1036 |
+
|
| 1037 |
+
ANALYSIS STEPS:
|
| 1038 |
+
1. Loaded multichannel microscopy image
|
| 1039 |
+
2. Extracted nuclei (channel {nuclei_channel}) and myofiber (channel {myofiber_channel}) signals
|
| 1040 |
+
3. Enhanced contrast using adaptive histogram equalization
|
| 1041 |
+
4. Segmented nuclei using {threshold_method} thresholding
|
| 1042 |
+
5. Segmented myofibers using {threshold_method} thresholding
|
| 1043 |
+
6. Performed morphological operations to refine segmentation
|
| 1044 |
+
7. Identified and measured individual myofibers and nuclei
|
| 1045 |
+
|
| 1046 |
+
RESULTS:
|
| 1047 |
+
- Total myofibers detected: {len(myofiber_props)}
|
| 1048 |
+
- Total nuclei detected: {nuclei_total}
|
| 1049 |
+
- Nuclei inside myofibers: {nuclei_inside} ({percent_inside:.2f}%)
|
| 1050 |
+
- Mean myofiber area: {mean_area:.2f} pixels
|
| 1051 |
+
- Mean myofiber perimeter: {mean_perimeter:.2f} pixels
|
| 1052 |
+
- Mean myofiber eccentricity: {mean_eccentricity:.2f}
|
| 1053 |
+
|
| 1054 |
+
FILES GENERATED:
|
| 1055 |
+
- Morphological measurements: {results_file}
|
| 1056 |
+
- Labeled myofiber image: {labeled_image_path}
|
| 1057 |
+
"""
|
| 1058 |
+
|
| 1059 |
+
return log
|
| 1060 |
+
|
| 1061 |
+
|
| 1062 |
+
def decode_behavior_from_neural_trajectories(neural_data, behavioral_data, n_components=10, output_dir="./"):
|
| 1063 |
+
"""Model neural activity trajectories and decode behavioral variables.
|
| 1064 |
+
|
| 1065 |
+
Parameters
|
| 1066 |
+
----------
|
| 1067 |
+
neural_data : numpy.ndarray
|
| 1068 |
+
Neural spiking activity data, shape (n_timepoints, n_neurons)
|
| 1069 |
+
behavioral_data : numpy.ndarray
|
| 1070 |
+
Behavioral data, shape (n_timepoints, n_behavioral_variables)
|
| 1071 |
+
n_components : int, optional
|
| 1072 |
+
Number of principal components to use for dimensionality reduction, default is 10
|
| 1073 |
+
output_dir : str, optional
|
| 1074 |
+
Directory to save output files, default is "./"
|
| 1075 |
+
|
| 1076 |
+
Returns
|
| 1077 |
+
-------
|
| 1078 |
+
str
|
| 1079 |
+
Research log summarizing the steps taken and results
|
| 1080 |
+
|
| 1081 |
+
"""
|
| 1082 |
+
import os
|
| 1083 |
+
import pickle
|
| 1084 |
+
|
| 1085 |
+
import matplotlib.pyplot as plt
|
| 1086 |
+
import numpy as np
|
| 1087 |
+
from pykalman import KalmanFilter
|
| 1088 |
+
from sklearn.decomposition import PCA
|
| 1089 |
+
from sklearn.metrics import mean_squared_error
|
| 1090 |
+
from sklearn.model_selection import train_test_split
|
| 1091 |
+
|
| 1092 |
+
# Create output directory if it doesn't exist
|
| 1093 |
+
if not os.path.exists(output_dir):
|
| 1094 |
+
os.makedirs(output_dir)
|
| 1095 |
+
|
| 1096 |
+
# Initialize research log
|
| 1097 |
+
log = "# Neural Trajectory Modeling and Decoding Research Log\n\n"
|
| 1098 |
+
|
| 1099 |
+
# Step 1: Preprocess the data
|
| 1100 |
+
log += "## Step 1: Data Preprocessing\n"
|
| 1101 |
+
log += f"- Neural data shape: {neural_data.shape}\n"
|
| 1102 |
+
log += f"- Behavioral data shape: {behavioral_data.shape}\n"
|
| 1103 |
+
|
| 1104 |
+
# Check for NaN values and replace with zeros
|
| 1105 |
+
neural_data = np.nan_to_num(neural_data)
|
| 1106 |
+
behavioral_data = np.nan_to_num(behavioral_data)
|
| 1107 |
+
|
| 1108 |
+
# Split data into training and testing sets
|
| 1109 |
+
X_train, X_test, y_train, y_test = train_test_split(neural_data, behavioral_data, test_size=0.2, random_state=42)
|
| 1110 |
+
log += f"- Training set size: {X_train.shape[0]} samples\n"
|
| 1111 |
+
log += f"- Testing set size: {X_test.shape[0]} samples\n\n"
|
| 1112 |
+
|
| 1113 |
+
# Step 2: Dimensionality reduction with PCA
|
| 1114 |
+
log += "## Step 2: Dimensionality Reduction\n"
|
| 1115 |
+
log += f"- Reducing neural data from {neural_data.shape[1]} dimensions to {n_components} components\n"
|
| 1116 |
+
|
| 1117 |
+
pca = PCA(n_components=n_components)
|
| 1118 |
+
X_train_pca = pca.fit_transform(X_train)
|
| 1119 |
+
X_test_pca = pca.transform(X_test)
|
| 1120 |
+
|
| 1121 |
+
explained_variance = np.sum(pca.explained_variance_ratio_) * 100
|
| 1122 |
+
log += f"- Total variance explained: {explained_variance:.2f}%\n\n"
|
| 1123 |
+
|
| 1124 |
+
# Save PCA components visualization
|
| 1125 |
+
try:
|
| 1126 |
+
plt.figure(figsize=(10, 6))
|
| 1127 |
+
plt.bar(range(1, n_components + 1), pca.explained_variance_ratio_)
|
| 1128 |
+
plt.xlabel("Principal Component")
|
| 1129 |
+
plt.ylabel("Explained Variance Ratio")
|
| 1130 |
+
plt.title("PCA Components Explained Variance")
|
| 1131 |
+
plt.xticks(range(1, n_components + 1))
|
| 1132 |
+
plt.tight_layout()
|
| 1133 |
+
|
| 1134 |
+
pca_plot_path = os.path.join(output_dir, "pca_explained_variance.png")
|
| 1135 |
+
plt.savefig(pca_plot_path, dpi=300)
|
| 1136 |
+
plt.close()
|
| 1137 |
+
|
| 1138 |
+
log += f"- PCA components visualization saved to: {pca_plot_path}\n\n"
|
| 1139 |
+
except Exception as e:
|
| 1140 |
+
log += f"- Error creating PCA visualization: {str(e)}\n\n"
|
| 1141 |
+
|
| 1142 |
+
# Step 3: Train a Kalman filter for decoding
|
| 1143 |
+
log += "## Step 3: Trajectory Modeling and Decoding\n"
|
| 1144 |
+
log += "- Training Kalman filter to decode behavioral variables from neural trajectories\n"
|
| 1145 |
+
|
| 1146 |
+
# Initialize and train Kalman filter
|
| 1147 |
+
kf = KalmanFilter(initial_state_mean=np.zeros(y_train.shape[1]), n_dim_obs=X_train_pca.shape[1])
|
| 1148 |
+
|
| 1149 |
+
# Fit the Kalman filter to the data
|
| 1150 |
+
kf.em(X_train_pca, y_train)
|
| 1151 |
+
|
| 1152 |
+
# Step 4: Decode behavioral variables
|
| 1153 |
+
log += "## Step 4: Decoding Behavioral Variables\n"
|
| 1154 |
+
|
| 1155 |
+
# Use the Kalman filter to predict behavioral variables
|
| 1156 |
+
y_pred, _ = kf.filter(X_test_pca)
|
| 1157 |
+
|
| 1158 |
+
# Evaluate performance
|
| 1159 |
+
mse = mean_squared_error(y_test, y_pred)
|
| 1160 |
+
log += f"- Mean squared error on test set: {mse:.4f}\n\n"
|
| 1161 |
+
|
| 1162 |
+
# Save the decoded trajectories visualization
|
| 1163 |
+
try:
|
| 1164 |
+
if y_test.shape[1] >= 2:
|
| 1165 |
+
# Create visualization of true vs. predicted trajectories (first 2 dimensions)
|
| 1166 |
+
plt.figure(figsize=(12, 6))
|
| 1167 |
+
|
| 1168 |
+
# First behavioral variable
|
| 1169 |
+
plt.subplot(1, 2, 1)
|
| 1170 |
+
plt.plot(y_test[:, 0], label="True")
|
| 1171 |
+
plt.plot(y_pred[:, 0], label="Predicted")
|
| 1172 |
+
plt.xlabel("Time steps")
|
| 1173 |
+
plt.ylabel("Behavioral Variable 1")
|
| 1174 |
+
plt.title("Decoding Performance - Variable 1")
|
| 1175 |
+
plt.legend()
|
| 1176 |
+
|
| 1177 |
+
# Second behavioral variable
|
| 1178 |
+
plt.subplot(1, 2, 2)
|
| 1179 |
+
plt.plot(y_test[:, 1], label="True")
|
| 1180 |
+
plt.plot(y_pred[:, 1], label="Predicted")
|
| 1181 |
+
plt.xlabel("Time steps")
|
| 1182 |
+
plt.ylabel("Behavioral Variable 2")
|
| 1183 |
+
plt.title("Decoding Performance - Variable 2")
|
| 1184 |
+
plt.legend()
|
| 1185 |
+
|
| 1186 |
+
plt.tight_layout()
|
| 1187 |
+
|
| 1188 |
+
trajectory_plot_path = os.path.join(output_dir, "decoded_trajectories.png")
|
| 1189 |
+
plt.savefig(trajectory_plot_path, dpi=300)
|
| 1190 |
+
plt.close()
|
| 1191 |
+
|
| 1192 |
+
log += f"- Decoded trajectories visualization saved to: {trajectory_plot_path}\n"
|
| 1193 |
+
except Exception as e:
|
| 1194 |
+
log += f"- Error creating trajectory visualization: {str(e)}\n"
|
| 1195 |
+
|
| 1196 |
+
# Save the results as a pickle file
|
| 1197 |
+
results = {
|
| 1198 |
+
"true_behavior": y_test,
|
| 1199 |
+
"predicted_behavior": y_pred,
|
| 1200 |
+
"pca_model": pca,
|
| 1201 |
+
"kalman_filter": kf,
|
| 1202 |
+
"mse": mse,
|
| 1203 |
+
}
|
| 1204 |
+
|
| 1205 |
+
results_file = os.path.join(output_dir, "neural_decoding_results.pkl")
|
| 1206 |
+
with open(results_file, "wb") as f:
|
| 1207 |
+
pickle.dump(results, f)
|
| 1208 |
+
|
| 1209 |
+
# Also save a CSV with the first few predicted vs. actual values for easier inspection
|
| 1210 |
+
try:
|
| 1211 |
+
import pandas as pd
|
| 1212 |
+
|
| 1213 |
+
n_samples = min(100, y_test.shape[0])
|
| 1214 |
+
n_vars = y_test.shape[1]
|
| 1215 |
+
|
| 1216 |
+
results_data = {}
|
| 1217 |
+
for i in range(n_vars):
|
| 1218 |
+
results_data[f"true_var{i + 1}"] = y_test[:n_samples, i]
|
| 1219 |
+
results_data[f"pred_var{i + 1}"] = y_pred[:n_samples, i]
|
| 1220 |
+
|
| 1221 |
+
results_df = pd.DataFrame(results_data)
|
| 1222 |
+
csv_path = os.path.join(output_dir, "decoding_results_sample.csv")
|
| 1223 |
+
results_df.to_csv(csv_path, index=False)
|
| 1224 |
+
|
| 1225 |
+
log += f"- Sample of decoding results saved to: {csv_path}\n"
|
| 1226 |
+
except Exception as e:
|
| 1227 |
+
log += f"- Error creating CSV results: {str(e)}\n"
|
| 1228 |
+
|
| 1229 |
+
log += "\n## Results\n"
|
| 1230 |
+
log += f"- Full decoded behavioral trajectories saved to: {results_file}\n"
|
| 1231 |
+
log += f"- Decoder performance (MSE): {mse:.4f}\n"
|
| 1232 |
+
|
| 1233 |
+
# Save the log to a file
|
| 1234 |
+
log_file = os.path.join(output_dir, "neural_decoding_log.txt")
|
| 1235 |
+
with open(log_file, "w") as f:
|
| 1236 |
+
f.write(log)
|
| 1237 |
+
|
| 1238 |
+
log += f"- Analysis log saved to: {log_file}\n"
|
| 1239 |
+
|
| 1240 |
+
return log
|
| 1241 |
+
|
| 1242 |
+
|
| 1243 |
+
def simulate_whole_cell_ode_model(
|
| 1244 |
+
initial_conditions,
|
| 1245 |
+
parameters,
|
| 1246 |
+
ode_function=None,
|
| 1247 |
+
time_span=(0, 100),
|
| 1248 |
+
time_points=1000,
|
| 1249 |
+
method="LSODA",
|
| 1250 |
+
):
|
| 1251 |
+
"""Simulate a whole-cell model represented as a system of ordinary differential equations (ODEs).
|
| 1252 |
+
|
| 1253 |
+
Parameters
|
| 1254 |
+
----------
|
| 1255 |
+
initial_conditions : dict or array-like
|
| 1256 |
+
Initial values for each state variable in the model. If dict, keys are variable names
|
| 1257 |
+
and values are initial concentrations/values. If array-like, order must match the
|
| 1258 |
+
order expected by the ODE function.
|
| 1259 |
+
parameters : dict
|
| 1260 |
+
Model parameters required by the ODE function. Keys are parameter names and
|
| 1261 |
+
values are parameter values.
|
| 1262 |
+
ode_function : callable, optional
|
| 1263 |
+
Function defining the system of ODEs. Should take arguments ``(t, y, *args)`` where
|
| 1264 |
+
``t`` is time, ``y`` is the state vector, and ``args`` contains additional parameters.
|
| 1265 |
+
If None, a simple example whole-cell model will be used.
|
| 1266 |
+
time_span : tuple, default=(0, 100)
|
| 1267 |
+
Tuple of (start_time, end_time) for the simulation.
|
| 1268 |
+
time_points : int, default=1000
|
| 1269 |
+
Number of time points to evaluate.
|
| 1270 |
+
method : str, default='LSODA'
|
| 1271 |
+
Numerical integration method to use (e.g., 'RK45', 'LSODA', 'BDF').
|
| 1272 |
+
|
| 1273 |
+
Returns
|
| 1274 |
+
-------
|
| 1275 |
+
str
|
| 1276 |
+
Research log summarizing the simulation steps and results. Results are saved
|
| 1277 |
+
to a CSV file and the filename is included in the log.
|
| 1278 |
+
|
| 1279 |
+
"""
|
| 1280 |
+
from datetime import datetime
|
| 1281 |
+
|
| 1282 |
+
import numpy as np
|
| 1283 |
+
import pandas as pd
|
| 1284 |
+
from scipy.integrate import solve_ivp
|
| 1285 |
+
|
| 1286 |
+
# Define a default ODE function if none is provided
|
| 1287 |
+
if ode_function is None:
|
| 1288 |
+
|
| 1289 |
+
def default_whole_cell_model(t, y, params):
|
| 1290 |
+
# Unpack state variables
|
| 1291 |
+
# Simple model with:
|
| 1292 |
+
# - mRNA (y[0])
|
| 1293 |
+
# - Protein (y[1])
|
| 1294 |
+
# - Metabolite (y[2])
|
| 1295 |
+
# - ATP (y[3])
|
| 1296 |
+
mRNA, protein, metabolite, atp = y
|
| 1297 |
+
|
| 1298 |
+
# Unpack parameters
|
| 1299 |
+
k_transcription = params["k_transcription"] # mRNA synthesis rate
|
| 1300 |
+
k_translation = params["k_translation"] # Protein synthesis rate
|
| 1301 |
+
k_mrna_deg = params["k_mrna_deg"] # mRNA degradation rate
|
| 1302 |
+
k_protein_deg = params["k_protein_deg"] # Protein degradation rate
|
| 1303 |
+
k_metabolism = params["k_metabolism"] # Metabolite production rate
|
| 1304 |
+
k_atp_production = params["k_atp_production"] # ATP production rate
|
| 1305 |
+
k_atp_consumption = params["k_atp_consumption"] # ATP consumption rate
|
| 1306 |
+
|
| 1307 |
+
# ODEs
|
| 1308 |
+
dmRNA_dt = k_transcription - k_mrna_deg * mRNA
|
| 1309 |
+
dprotein_dt = k_translation * mRNA * atp - k_protein_deg * protein
|
| 1310 |
+
dmetabolite_dt = k_metabolism * protein - k_atp_production * metabolite
|
| 1311 |
+
datp_dt = k_atp_production * metabolite - k_atp_consumption * atp - k_translation * mRNA * atp
|
| 1312 |
+
|
| 1313 |
+
return [dmRNA_dt, dprotein_dt, dmetabolite_dt, datp_dt]
|
| 1314 |
+
|
| 1315 |
+
ode_function = default_whole_cell_model
|
| 1316 |
+
|
| 1317 |
+
# Prepare initial conditions as array
|
| 1318 |
+
if isinstance(initial_conditions, dict):
|
| 1319 |
+
y0_values = list(initial_conditions.values())
|
| 1320 |
+
variable_names = list(initial_conditions.keys())
|
| 1321 |
+
else:
|
| 1322 |
+
y0_values = initial_conditions
|
| 1323 |
+
variable_names = [f"Variable_{i}" for i in range(len(initial_conditions))]
|
| 1324 |
+
|
| 1325 |
+
# Set up time points
|
| 1326 |
+
t_eval = np.linspace(time_span[0], time_span[1], time_points)
|
| 1327 |
+
|
| 1328 |
+
# Start research log
|
| 1329 |
+
log = []
|
| 1330 |
+
log.append("# Whole-Cell ODE Model Simulation")
|
| 1331 |
+
log.append(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 1332 |
+
log.append("\n## Simulation Setup")
|
| 1333 |
+
log.append(f"- Integration method: {method}")
|
| 1334 |
+
log.append(f"- Time span: {time_span[0]} to {time_span[1]} time units")
|
| 1335 |
+
log.append(f"- Number of time points: {time_points}")
|
| 1336 |
+
log.append(f"- Number of state variables: {len(y0_values)}")
|
| 1337 |
+
log.append("\n## Initial Conditions")
|
| 1338 |
+
for _i, (name, value) in enumerate(zip(variable_names, y0_values, strict=False)):
|
| 1339 |
+
log.append(f"- {name}: {value}")
|
| 1340 |
+
|
| 1341 |
+
log.append("\n## Model Parameters")
|
| 1342 |
+
for param, value in parameters.items():
|
| 1343 |
+
log.append(f"- {param}: {value}")
|
| 1344 |
+
|
| 1345 |
+
# Solve the ODE system
|
| 1346 |
+
log.append("\n## Running Simulation")
|
| 1347 |
+
try:
|
| 1348 |
+
solution = solve_ivp(
|
| 1349 |
+
lambda t, y: ode_function(t, y, parameters),
|
| 1350 |
+
time_span,
|
| 1351 |
+
y0_values,
|
| 1352 |
+
method=method,
|
| 1353 |
+
t_eval=t_eval,
|
| 1354 |
+
)
|
| 1355 |
+
|
| 1356 |
+
# Check if simulation was successful
|
| 1357 |
+
if solution.success:
|
| 1358 |
+
log.append("Simulation completed successfully.")
|
| 1359 |
+
log.append(f"- Number of function evaluations: {solution.nfev}")
|
| 1360 |
+
log.append(f"- Number of Jacobian evaluations: {solution.njev}")
|
| 1361 |
+
log.append(f"- Number of steps: {len(solution.t)}")
|
| 1362 |
+
|
| 1363 |
+
# Create DataFrame with results
|
| 1364 |
+
results_df = pd.DataFrame(solution.y.T, columns=variable_names)
|
| 1365 |
+
results_df.insert(0, "Time", solution.t)
|
| 1366 |
+
|
| 1367 |
+
# Save results to CSV
|
| 1368 |
+
filename = f"whole_cell_simulation_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 1369 |
+
results_df.to_csv(filename, index=False)
|
| 1370 |
+
|
| 1371 |
+
log.append("\n## Results Summary")
|
| 1372 |
+
log.append(f"Simulation results saved to: {filename}")
|
| 1373 |
+
|
| 1374 |
+
# Calculate some basic statistics
|
| 1375 |
+
final_state = results_df.iloc[-1].drop("Time").to_dict()
|
| 1376 |
+
log.append("\n## Final State")
|
| 1377 |
+
for var, value in final_state.items():
|
| 1378 |
+
log.append(f"- {var}: {value:.6f}")
|
| 1379 |
+
|
| 1380 |
+
else:
|
| 1381 |
+
log.append(f"Simulation failed with message: {solution.message}")
|
| 1382 |
+
|
| 1383 |
+
except Exception as e:
|
| 1384 |
+
log.append(f"Error during simulation: {str(e)}")
|
| 1385 |
+
|
| 1386 |
+
# Return the research log
|
| 1387 |
+
return "\n".join(log)
|
Biomanus_upload/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)
|
Biomanus_upload/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
|
Biomanus_upload/biomni/tool/cancer_biology.py
ADDED
|
@@ -0,0 +1,1292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def analyze_ddr_network_in_cancer(expression_data_path, mutation_data_path, output_dir="./results"):
|
| 2 |
+
"""Analyze DNA Damage Response (DDR) network alterations and dependencies in cancer samples.
|
| 3 |
+
|
| 4 |
+
This function reconstructs the DDR network from genomic data, identifies disruptions
|
| 5 |
+
in the network, and analyzes dependencies between DDR pathway components in cancer.
|
| 6 |
+
|
| 7 |
+
Parameters
|
| 8 |
+
----------
|
| 9 |
+
expression_data_path : str
|
| 10 |
+
Path to gene expression data file (CSV format with genes as rows, samples as columns)
|
| 11 |
+
mutation_data_path : str
|
| 12 |
+
Path to mutation data file (CSV format with genes as rows, samples as columns,
|
| 13 |
+
values indicating mutation status)
|
| 14 |
+
output_dir : str, optional
|
| 15 |
+
Directory to save output files (default: "./results")
|
| 16 |
+
|
| 17 |
+
Returns
|
| 18 |
+
-------
|
| 19 |
+
str
|
| 20 |
+
Research log summarizing the DDR network analysis, findings about disruptions,
|
| 21 |
+
and potential therapeutic vulnerabilities
|
| 22 |
+
|
| 23 |
+
"""
|
| 24 |
+
import os
|
| 25 |
+
from datetime import datetime
|
| 26 |
+
|
| 27 |
+
import gseapy as gp
|
| 28 |
+
import networkx as nx
|
| 29 |
+
import pandas as pd
|
| 30 |
+
from scipy.stats import pearsonr
|
| 31 |
+
|
| 32 |
+
# Create output directory if it doesn't exist
|
| 33 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 34 |
+
|
| 35 |
+
research_log = f"DDR Network Analysis Research Log - {datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
|
| 36 |
+
research_log += "=" * 80 + "\n\n"
|
| 37 |
+
|
| 38 |
+
# Step 1: Load and preprocess genomic data
|
| 39 |
+
research_log += "STEP 1: Loading and preprocessing genomic data\n"
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
expr_data = pd.read_csv(expression_data_path, index_col=0)
|
| 43 |
+
mut_data = pd.read_csv(mutation_data_path, index_col=0)
|
| 44 |
+
|
| 45 |
+
research_log += f"- Loaded expression data: {expr_data.shape[0]} genes × {expr_data.shape[1]} samples\n"
|
| 46 |
+
research_log += f"- Loaded mutation data: {mut_data.shape[0]} genes × {mut_data.shape[1]} samples\n"
|
| 47 |
+
|
| 48 |
+
# Define core DDR genes (based on literature)
|
| 49 |
+
ddr_genes = [
|
| 50 |
+
# DNA damage sensors
|
| 51 |
+
"ATM",
|
| 52 |
+
"ATR",
|
| 53 |
+
"PRKDC",
|
| 54 |
+
"RAD50",
|
| 55 |
+
"MRE11",
|
| 56 |
+
"NBN",
|
| 57 |
+
# Signal transducers
|
| 58 |
+
"CHEK1",
|
| 59 |
+
"CHEK2",
|
| 60 |
+
"TP53",
|
| 61 |
+
"BRCA1",
|
| 62 |
+
"BRCA2",
|
| 63 |
+
"MDC1",
|
| 64 |
+
"H2AX",
|
| 65 |
+
# Effectors - Homologous Recombination
|
| 66 |
+
"RAD51",
|
| 67 |
+
"RAD52",
|
| 68 |
+
"PALB2",
|
| 69 |
+
"RAD54L",
|
| 70 |
+
# Effectors - Non-Homologous End Joining
|
| 71 |
+
"XRCC4",
|
| 72 |
+
"LIG4",
|
| 73 |
+
"XRCC5",
|
| 74 |
+
"XRCC6",
|
| 75 |
+
# Effectors - Base Excision Repair
|
| 76 |
+
"PARP1",
|
| 77 |
+
"APEX1",
|
| 78 |
+
"OGG1",
|
| 79 |
+
"XRCC1",
|
| 80 |
+
# Effectors - Nucleotide Excision Repair
|
| 81 |
+
"XPA",
|
| 82 |
+
"XPC",
|
| 83 |
+
"ERCC1",
|
| 84 |
+
"ERCC2",
|
| 85 |
+
"ERCC3",
|
| 86 |
+
"ERCC4",
|
| 87 |
+
"ERCC5",
|
| 88 |
+
# Effectors - Mismatch Repair
|
| 89 |
+
"MLH1",
|
| 90 |
+
"MSH2",
|
| 91 |
+
"MSH6",
|
| 92 |
+
"PMS2",
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
# Filter expression and mutation data for DDR genes
|
| 96 |
+
ddr_expr = expr_data.loc[expr_data.index.isin(ddr_genes)]
|
| 97 |
+
ddr_mut = mut_data.loc[mut_data.index.isin(ddr_genes)]
|
| 98 |
+
|
| 99 |
+
research_log += f"- Filtered data for {len(ddr_genes)} DDR pathway genes\n"
|
| 100 |
+
research_log += f"- Found {ddr_expr.shape[0]} DDR genes in expression data\n"
|
| 101 |
+
research_log += f"- Found {ddr_mut.shape[0]} DDR genes in mutation data\n\n"
|
| 102 |
+
|
| 103 |
+
except Exception as e:
|
| 104 |
+
research_log += f"Error in data loading: {str(e)}\n\n"
|
| 105 |
+
return research_log
|
| 106 |
+
|
| 107 |
+
# Step 2: Network reconstruction
|
| 108 |
+
research_log += "STEP 2: Reconstructing DDR gene network\n"
|
| 109 |
+
|
| 110 |
+
# Create correlation-based network
|
| 111 |
+
G = nx.Graph()
|
| 112 |
+
|
| 113 |
+
# Add nodes (genes)
|
| 114 |
+
for gene in ddr_expr.index:
|
| 115 |
+
# Add node with mutation frequency information
|
| 116 |
+
mutation_freq = ddr_mut.loc[gene].mean() if gene in ddr_mut.index else 0
|
| 117 |
+
|
| 118 |
+
G.add_node(gene, mutation_freq=mutation_freq)
|
| 119 |
+
|
| 120 |
+
# Add edges based on gene expression correlation
|
| 121 |
+
for i, gene1 in enumerate(ddr_expr.index):
|
| 122 |
+
for gene2 in ddr_expr.index[i + 1 :]:
|
| 123 |
+
corr, p_value = pearsonr(ddr_expr.loc[gene1], ddr_expr.loc[gene2])
|
| 124 |
+
if abs(corr) > 0.4 and p_value < 0.05: # Significant correlation threshold
|
| 125 |
+
G.add_edge(gene1, gene2, weight=abs(corr), correlation=corr)
|
| 126 |
+
|
| 127 |
+
research_log += f"- Constructed network with {G.number_of_nodes()} nodes and {G.number_of_edges()} edges\n"
|
| 128 |
+
|
| 129 |
+
# Save network for visualization
|
| 130 |
+
network_file = os.path.join(output_dir, "ddr_network.graphml")
|
| 131 |
+
nx.write_graphml(G, network_file)
|
| 132 |
+
research_log += f"- Network saved to: {network_file}\n\n"
|
| 133 |
+
|
| 134 |
+
# Step 3: Network analysis
|
| 135 |
+
research_log += "STEP 3: Analyzing DDR network properties\n"
|
| 136 |
+
|
| 137 |
+
# Calculate network centrality measures
|
| 138 |
+
degree_centrality = nx.degree_centrality(G)
|
| 139 |
+
betweenness_centrality = nx.betweenness_centrality(G)
|
| 140 |
+
|
| 141 |
+
# Identify hub genes (high degree centrality)
|
| 142 |
+
hub_genes = sorted(degree_centrality.items(), key=lambda x: x[1], reverse=True)[:5]
|
| 143 |
+
|
| 144 |
+
research_log += "- Top 5 hub genes (highest connectivity):\n"
|
| 145 |
+
for gene, centrality in hub_genes:
|
| 146 |
+
research_log += f" * {gene}: {centrality:.4f}\n"
|
| 147 |
+
|
| 148 |
+
# Identify bottleneck genes (high betweenness centrality)
|
| 149 |
+
bottleneck_genes = sorted(betweenness_centrality.items(), key=lambda x: x[1], reverse=True)[:5]
|
| 150 |
+
|
| 151 |
+
research_log += "- Top 5 bottleneck genes (critical for information flow):\n"
|
| 152 |
+
for gene, centrality in bottleneck_genes:
|
| 153 |
+
research_log += f" * {gene}: {centrality:.4f}\n"
|
| 154 |
+
|
| 155 |
+
# Identify frequently mutated DDR genes
|
| 156 |
+
mutation_freq = {node: G.nodes[node]["mutation_freq"] for node in G.nodes()}
|
| 157 |
+
frequently_mutated = sorted(mutation_freq.items(), key=lambda x: x[1], reverse=True)[:5]
|
| 158 |
+
|
| 159 |
+
research_log += "- Top 5 frequently mutated DDR genes:\n"
|
| 160 |
+
for gene, freq in frequently_mutated:
|
| 161 |
+
research_log += f" * {gene}: {freq:.4f}\n\n"
|
| 162 |
+
|
| 163 |
+
# Step 4: Community detection to identify DDR sub-pathways
|
| 164 |
+
research_log += "STEP 4: Identifying DDR sub-pathways through community detection\n"
|
| 165 |
+
|
| 166 |
+
# Use Louvain method for community detection
|
| 167 |
+
try:
|
| 168 |
+
from community import best_partition
|
| 169 |
+
|
| 170 |
+
partition = best_partition(G)
|
| 171 |
+
communities = {}
|
| 172 |
+
for node, community_id in partition.items():
|
| 173 |
+
if community_id not in communities:
|
| 174 |
+
communities[community_id] = []
|
| 175 |
+
communities[community_id].append(node)
|
| 176 |
+
|
| 177 |
+
research_log += f"- Identified {len(communities)} DDR sub-pathways\n"
|
| 178 |
+
|
| 179 |
+
for i, (_, genes) in enumerate(communities.items()):
|
| 180 |
+
research_log += f" * Sub-pathway {i + 1}: {', '.join(genes)}\n"
|
| 181 |
+
|
| 182 |
+
except ImportError:
|
| 183 |
+
research_log += "- Community detection skipped (python-louvain package not installed)\n"
|
| 184 |
+
|
| 185 |
+
# Step 5: Identify disrupted DDR pathways using GSEA
|
| 186 |
+
research_log += "\nSTEP 5: Pathway enrichment analysis of DDR genes\n"
|
| 187 |
+
|
| 188 |
+
try:
|
| 189 |
+
# Create gene list for GSEA
|
| 190 |
+
gene_list = ddr_expr.mean(axis=1).sort_values(ascending=False)
|
| 191 |
+
|
| 192 |
+
# Run GSEA with GO Biological Process
|
| 193 |
+
enrichr_results = gp.enrichr(
|
| 194 |
+
gene_list=gene_list.index.tolist(),
|
| 195 |
+
gene_sets=["GO_Biological_Process_2021"],
|
| 196 |
+
outdir=os.path.join(output_dir, "enrichr_output"),
|
| 197 |
+
cutoff=0.5,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
# Filter for DNA repair related terms
|
| 201 |
+
ddr_terms = [
|
| 202 |
+
term
|
| 203 |
+
for term in enrichr_results.results["Term"]
|
| 204 |
+
if any(x in term.lower() for x in ["dna repair", "damage", "recombination", "checkpoint"])
|
| 205 |
+
]
|
| 206 |
+
|
| 207 |
+
if ddr_terms:
|
| 208 |
+
research_log += "- Enriched DDR-related pathways:\n"
|
| 209 |
+
for term in ddr_terms[:5]: # Top 5 DDR-related terms
|
| 210 |
+
research_log += f" * {term}\n"
|
| 211 |
+
else:
|
| 212 |
+
research_log += "- No significant DDR pathway enrichment found\n"
|
| 213 |
+
|
| 214 |
+
# Save full enrichment results
|
| 215 |
+
enrichment_file = os.path.join(output_dir, "ddr_pathway_enrichment.csv")
|
| 216 |
+
enrichr_results.results.to_csv(enrichment_file)
|
| 217 |
+
research_log += f"- Full enrichment results saved to: {enrichment_file}\n\n"
|
| 218 |
+
|
| 219 |
+
except Exception as e:
|
| 220 |
+
research_log += f"- Pathway enrichment analysis error: {str(e)}\n\n"
|
| 221 |
+
|
| 222 |
+
# Step 6: Summary of findings
|
| 223 |
+
research_log += "SUMMARY OF FINDINGS\n"
|
| 224 |
+
research_log += "=" * 80 + "\n"
|
| 225 |
+
|
| 226 |
+
# Key hub genes (potential therapeutic targets)
|
| 227 |
+
research_log += "1. Key DDR hub genes that may serve as potential therapeutic targets:\n"
|
| 228 |
+
for gene, _ in hub_genes[:3]:
|
| 229 |
+
research_log += f" - {gene}\n"
|
| 230 |
+
|
| 231 |
+
# Frequently mutated genes
|
| 232 |
+
research_log += "\n2. Frequently mutated DDR genes in the samples:\n"
|
| 233 |
+
for gene, freq in frequently_mutated[:3]:
|
| 234 |
+
research_log += f" - {gene} (mutation frequency: {freq:.2%})\n"
|
| 235 |
+
|
| 236 |
+
# Network structure insights
|
| 237 |
+
research_log += "\n3. DDR network structure insights:\n"
|
| 238 |
+
research_log += f" - Network density: {nx.density(G):.4f}\n"
|
| 239 |
+
|
| 240 |
+
try:
|
| 241 |
+
avg_clustering = nx.average_clustering(G)
|
| 242 |
+
research_log += f" - Average clustering coefficient: {avg_clustering:.4f}\n"
|
| 243 |
+
except Exception:
|
| 244 |
+
pass
|
| 245 |
+
|
| 246 |
+
research_log += "\n4. Potential DDR dependencies and synthetic lethality targets:\n"
|
| 247 |
+
# Identify potential synthetic lethality pairs (genes with high correlation and one is frequently mutated)
|
| 248 |
+
synthetic_lethality_candidates = []
|
| 249 |
+
for gene1, gene2, data in G.edges(data=True):
|
| 250 |
+
if abs(data["correlation"]) > 0.6: # Strong correlation
|
| 251 |
+
mut_freq1 = G.nodes[gene1]["mutation_freq"]
|
| 252 |
+
mut_freq2 = G.nodes[gene2]["mutation_freq"]
|
| 253 |
+
if (mut_freq1 > 0.1 and mut_freq2 < 0.05) or (mut_freq2 > 0.1 and mut_freq1 < 0.05):
|
| 254 |
+
if mut_freq1 > mut_freq2:
|
| 255 |
+
synthetic_lethality_candidates.append((gene1, gene2, data["correlation"], mut_freq1))
|
| 256 |
+
else:
|
| 257 |
+
synthetic_lethality_candidates.append((gene2, gene1, data["correlation"], mut_freq2))
|
| 258 |
+
|
| 259 |
+
if synthetic_lethality_candidates:
|
| 260 |
+
for mutated, target, _, _ in sorted(synthetic_lethality_candidates, key=lambda x: x[3], reverse=True)[:3]:
|
| 261 |
+
research_log += f" - {mutated} (frequently mutated) and {target} (potential dependency)\n"
|
| 262 |
+
else:
|
| 263 |
+
research_log += " - No strong synthetic lethality candidates identified\n"
|
| 264 |
+
|
| 265 |
+
return research_log
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def analyze_cell_senescence_and_apoptosis(fcs_file_path):
|
| 269 |
+
"""Analyze flow cytometry data to quantify senescent and apoptotic cell populations.
|
| 270 |
+
|
| 271 |
+
Parameters
|
| 272 |
+
----------
|
| 273 |
+
fcs_file_path : str
|
| 274 |
+
Path to the FCS file containing flow cytometry data with measurements for
|
| 275 |
+
senescence-associated β-galactosidase (SA-β-Gal) and Annexin V/7-AAD staining
|
| 276 |
+
|
| 277 |
+
Returns
|
| 278 |
+
-------
|
| 279 |
+
str
|
| 280 |
+
A research log summarizing the analysis steps and results, including percentages
|
| 281 |
+
of senescent and apoptotic cell populations
|
| 282 |
+
|
| 283 |
+
"""
|
| 284 |
+
import os
|
| 285 |
+
|
| 286 |
+
import numpy as np
|
| 287 |
+
from FlowCytometryTools import FCMeasurement
|
| 288 |
+
|
| 289 |
+
log = "# Flow Cytometry Analysis of Cell Senescence and Apoptosis\n\n"
|
| 290 |
+
|
| 291 |
+
try:
|
| 292 |
+
# Step 1: Load the FCS file
|
| 293 |
+
log += "## Step 1: Loading Flow Cytometry Data\n"
|
| 294 |
+
log += f"- Loading FCS file from: {fcs_file_path}\n"
|
| 295 |
+
|
| 296 |
+
sample = FCMeasurement(ID="Sample", datafile=fcs_file_path)
|
| 297 |
+
log += f"- Successfully loaded data with {len(sample)} events\n"
|
| 298 |
+
log += f"- Available channels: {', '.join(sample.channel_names)}\n\n"
|
| 299 |
+
|
| 300 |
+
# Step 2: Apply compensation if needed (assuming pre-compensated data)
|
| 301 |
+
log += "## Step 2: Data Preprocessing\n"
|
| 302 |
+
log += "- Checking for outliers and debris based on forward/side scatter\n"
|
| 303 |
+
|
| 304 |
+
# Basic filtering to remove debris based on FSC and SSC
|
| 305 |
+
# Assuming FSC-A and SSC-A are the channel names (adjust if different)
|
| 306 |
+
fsc_channel = "FSC-A" if "FSC-A" in sample.channel_names else sample.channel_names[0]
|
| 307 |
+
ssc_channel = "SSC-A" if "SSC-A" in sample.channel_names else sample.channel_names[1]
|
| 308 |
+
|
| 309 |
+
# Filter out debris (low FSC and SSC)
|
| 310 |
+
sample_filtered = sample.gate(f"{fsc_channel} > 10000 and {ssc_channel} > 5000")
|
| 311 |
+
|
| 312 |
+
log += f"- Filtered out debris: {len(sample)} → {len(sample_filtered)} events ({len(sample_filtered) / len(sample) * 100:.1f}%)\n\n"
|
| 313 |
+
|
| 314 |
+
# Step 3: Identify senescent cells (SA-β-Gal positive)
|
| 315 |
+
log += "## Step 3: Identifying Senescent Cells (SA-β-Gal+)\n"
|
| 316 |
+
|
| 317 |
+
# Find SA-β-Gal channel (adjust based on actual channel name)
|
| 318 |
+
sa_bgal_channel = None
|
| 319 |
+
for channel in sample_filtered.channel_names:
|
| 320 |
+
if "GAL" in channel.upper() or "FITC" in channel.upper():
|
| 321 |
+
sa_bgal_channel = channel
|
| 322 |
+
break
|
| 323 |
+
|
| 324 |
+
if not sa_bgal_channel:
|
| 325 |
+
log += "- WARNING: Could not identify SA-β-Gal channel. Using first fluorescence channel as placeholder.\n"
|
| 326 |
+
# Use the first fluorescence channel as a fallback
|
| 327 |
+
for channel in sample_filtered.channel_names:
|
| 328 |
+
if any(x in channel.upper() for x in ["FL", "BL", "FITC", "PE", "APC"]):
|
| 329 |
+
sa_bgal_channel = channel
|
| 330 |
+
break
|
| 331 |
+
|
| 332 |
+
log += f"- Using {sa_bgal_channel} as SA-β-Gal activity indicator\n"
|
| 333 |
+
|
| 334 |
+
# Determine threshold for SA-β-Gal positivity (using a simple percentile approach)
|
| 335 |
+
# In practice, this would be based on controls or known thresholds
|
| 336 |
+
sa_bgal_threshold = np.percentile(sample_filtered.data[sa_bgal_channel], 80)
|
| 337 |
+
senescent_cells = sample_filtered.gate(f"{sa_bgal_channel} > {sa_bgal_threshold}")
|
| 338 |
+
|
| 339 |
+
senescent_percentage = (len(senescent_cells) / len(sample_filtered)) * 100
|
| 340 |
+
log += f"- Applied threshold at {sa_bgal_threshold:.1f} fluorescence intensity\n"
|
| 341 |
+
log += f"- Identified {len(senescent_cells)} senescent cells ({senescent_percentage:.2f}%)\n\n"
|
| 342 |
+
|
| 343 |
+
# Step 4: Identify apoptotic cells (Annexin V+/7-AAD+)
|
| 344 |
+
log += "## Step 4: Identifying Apoptotic Cells (Annexin V/7-AAD)\n"
|
| 345 |
+
|
| 346 |
+
# Find Annexin V and 7-AAD channels (adjust based on actual channel names)
|
| 347 |
+
annexin_channel = None
|
| 348 |
+
aad_channel = None
|
| 349 |
+
|
| 350 |
+
for channel in sample_filtered.channel_names:
|
| 351 |
+
if "ANNEXIN" in channel.upper() or "PE" in channel.upper():
|
| 352 |
+
annexin_channel = channel
|
| 353 |
+
if "7AAD" in channel.upper() or "AAD" in channel.upper() or "PerCP" in channel.upper():
|
| 354 |
+
aad_channel = channel
|
| 355 |
+
|
| 356 |
+
if not annexin_channel or not aad_channel:
|
| 357 |
+
log += "- WARNING: Could not identify Annexin V and/or 7-AAD channels. Using placeholder channels.\n"
|
| 358 |
+
# Use fallback channels
|
| 359 |
+
fluorescence_channels = [
|
| 360 |
+
ch
|
| 361 |
+
for ch in sample_filtered.channel_names
|
| 362 |
+
if any(x in ch.upper() for x in ["FL", "BL", "FITC", "PE", "APC", "PerCP"])
|
| 363 |
+
]
|
| 364 |
+
if len(fluorescence_channels) >= 2:
|
| 365 |
+
if not annexin_channel:
|
| 366 |
+
annexin_channel = fluorescence_channels[0]
|
| 367 |
+
if not aad_channel:
|
| 368 |
+
aad_channel = fluorescence_channels[1]
|
| 369 |
+
|
| 370 |
+
log += f"- Using {annexin_channel} as Annexin V indicator\n"
|
| 371 |
+
log += f"- Using {aad_channel} as 7-AAD indicator\n"
|
| 372 |
+
|
| 373 |
+
# Determine thresholds for Annexin V and 7-AAD (using simple percentile approach)
|
| 374 |
+
annexin_threshold = np.percentile(sample_filtered.data[annexin_channel], 90)
|
| 375 |
+
aad_threshold = np.percentile(sample_filtered.data[aad_channel], 90)
|
| 376 |
+
|
| 377 |
+
# Early apoptotic: Annexin V+ / 7-AAD-
|
| 378 |
+
early_apoptotic = sample_filtered.gate(
|
| 379 |
+
f"{annexin_channel} > {annexin_threshold} and {aad_channel} < {aad_threshold}"
|
| 380 |
+
)
|
| 381 |
+
early_apoptotic_percentage = (len(early_apoptotic) / len(sample_filtered)) * 100
|
| 382 |
+
|
| 383 |
+
# Late apoptotic/necrotic: Annexin V+ / 7-AAD+
|
| 384 |
+
late_apoptotic = sample_filtered.gate(
|
| 385 |
+
f"{annexin_channel} > {annexin_threshold} and {aad_channel} > {aad_threshold}"
|
| 386 |
+
)
|
| 387 |
+
late_apoptotic_percentage = (len(late_apoptotic) / len(sample_filtered)) * 100
|
| 388 |
+
|
| 389 |
+
# Total apoptotic (early + late)
|
| 390 |
+
total_apoptotic_percentage = early_apoptotic_percentage + late_apoptotic_percentage
|
| 391 |
+
|
| 392 |
+
log += f"- Early apoptotic cells (Annexin V+/7-AAD-): {early_apoptotic_percentage:.2f}%\n"
|
| 393 |
+
log += f"- Late apoptotic cells (Annexin V+/7-AAD+): {late_apoptotic_percentage:.2f}%\n"
|
| 394 |
+
log += f"- Total apoptotic cells: {total_apoptotic_percentage:.2f}%\n\n"
|
| 395 |
+
|
| 396 |
+
# Step 5: Summary of results
|
| 397 |
+
log += "## Summary of Results\n"
|
| 398 |
+
log += f"- Total events analyzed: {len(sample_filtered)}\n"
|
| 399 |
+
log += f"- Senescent cells (SA-β-Gal+): {senescent_percentage:.2f}%\n"
|
| 400 |
+
log += f"- Early apoptotic cells (Annexin V+/7-AAD-): {early_apoptotic_percentage:.2f}%\n"
|
| 401 |
+
log += f"- Late apoptotic cells (Annexin V+/7-AAD+): {late_apoptotic_percentage:.2f}%\n"
|
| 402 |
+
log += f"- Total apoptotic cells: {total_apoptotic_percentage:.2f}%\n"
|
| 403 |
+
|
| 404 |
+
# Save results to CSV file
|
| 405 |
+
results_file = os.path.splitext(os.path.basename(fcs_file_path))[0] + "_results.csv"
|
| 406 |
+
with open(results_file, "w") as f:
|
| 407 |
+
f.write("Cell Population,Percentage\n")
|
| 408 |
+
f.write(f"Senescent cells,{senescent_percentage:.2f}\n")
|
| 409 |
+
f.write(f"Early apoptotic cells,{early_apoptotic_percentage:.2f}\n")
|
| 410 |
+
f.write(f"Late apoptotic cells,{late_apoptotic_percentage:.2f}\n")
|
| 411 |
+
f.write(f"Total apoptotic cells,{total_apoptotic_percentage:.2f}\n")
|
| 412 |
+
|
| 413 |
+
log += f"\nResults saved to: {results_file}\n"
|
| 414 |
+
|
| 415 |
+
except Exception as e:
|
| 416 |
+
log += "\n## ERROR: An error occurred during analysis\n"
|
| 417 |
+
log += f"- Error message: {str(e)}\n"
|
| 418 |
+
|
| 419 |
+
return log
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def detect_and_annotate_somatic_mutations(
|
| 423 |
+
tumor_bam, normal_bam, reference_genome, output_prefix, snpeff_database="GRCh38.105"
|
| 424 |
+
):
|
| 425 |
+
"""Detects and annotates somatic mutations in tumor samples compared to matched normal samples.
|
| 426 |
+
|
| 427 |
+
This function uses GATK Mutect2 for variant calling, GATK FilterMutectCalls for filtering,
|
| 428 |
+
and SnpEff for functional annotation of somatic mutations.
|
| 429 |
+
|
| 430 |
+
Parameters
|
| 431 |
+
----------
|
| 432 |
+
tumor_bam : str
|
| 433 |
+
Path to the tumor sample BAM file
|
| 434 |
+
normal_bam : str
|
| 435 |
+
Path to the matched normal sample BAM file
|
| 436 |
+
reference_genome : str
|
| 437 |
+
Path to the reference genome FASTA file
|
| 438 |
+
output_prefix : str
|
| 439 |
+
Prefix for output files
|
| 440 |
+
snpeff_database : str, optional
|
| 441 |
+
SnpEff database to use for annotation (default: "GRCh38.105")
|
| 442 |
+
|
| 443 |
+
Returns
|
| 444 |
+
-------
|
| 445 |
+
str
|
| 446 |
+
A research log summarizing the steps performed and results obtained
|
| 447 |
+
|
| 448 |
+
"""
|
| 449 |
+
import datetime
|
| 450 |
+
import os
|
| 451 |
+
import subprocess
|
| 452 |
+
|
| 453 |
+
# Initialize research log
|
| 454 |
+
log = "# Somatic Mutation Analysis Log\n"
|
| 455 |
+
log += f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
| 456 |
+
log += "## Input Files\n"
|
| 457 |
+
log += f"- Tumor BAM: {tumor_bam}\n"
|
| 458 |
+
log += f"- Normal BAM: {normal_bam}\n"
|
| 459 |
+
log += f"- Reference Genome: {reference_genome}\n\n"
|
| 460 |
+
|
| 461 |
+
# Step 1: Run Mutect2 for somatic variant calling
|
| 462 |
+
log += "## Step 1: Somatic Variant Calling with Mutect2\n"
|
| 463 |
+
raw_vcf = f"{output_prefix}.unfiltered.vcf"
|
| 464 |
+
|
| 465 |
+
mutect2_cmd = [
|
| 466 |
+
"gatk",
|
| 467 |
+
"Mutect2",
|
| 468 |
+
"-R",
|
| 469 |
+
reference_genome,
|
| 470 |
+
"-I",
|
| 471 |
+
tumor_bam,
|
| 472 |
+
"-I",
|
| 473 |
+
normal_bam,
|
| 474 |
+
"-normal",
|
| 475 |
+
os.path.basename(normal_bam).split(".")[0],
|
| 476 |
+
"-O",
|
| 477 |
+
raw_vcf,
|
| 478 |
+
]
|
| 479 |
+
|
| 480 |
+
log += f"Running command: {' '.join(mutect2_cmd)}\n"
|
| 481 |
+
try:
|
| 482 |
+
subprocess.run(mutect2_cmd, check=True, capture_output=True)
|
| 483 |
+
log += f"Successfully generated raw VCF: {raw_vcf}\n\n"
|
| 484 |
+
except subprocess.CalledProcessError as e:
|
| 485 |
+
log += f"Error running Mutect2: {str(e)}\n"
|
| 486 |
+
return log
|
| 487 |
+
|
| 488 |
+
# Step 2: Filter somatic calls
|
| 489 |
+
log += "## Step 2: Filtering Somatic Variants\n"
|
| 490 |
+
filtered_vcf = f"{output_prefix}.filtered.vcf"
|
| 491 |
+
|
| 492 |
+
filter_cmd = [
|
| 493 |
+
"gatk",
|
| 494 |
+
"FilterMutectCalls",
|
| 495 |
+
"-R",
|
| 496 |
+
reference_genome,
|
| 497 |
+
"-V",
|
| 498 |
+
raw_vcf,
|
| 499 |
+
"-O",
|
| 500 |
+
filtered_vcf,
|
| 501 |
+
]
|
| 502 |
+
|
| 503 |
+
log += f"Running command: {' '.join(filter_cmd)}\n"
|
| 504 |
+
try:
|
| 505 |
+
subprocess.run(filter_cmd, check=True, capture_output=True)
|
| 506 |
+
log += f"Successfully filtered variants: {filtered_vcf}\n\n"
|
| 507 |
+
except subprocess.CalledProcessError as e:
|
| 508 |
+
log += f"Error filtering variants: {str(e)}\n"
|
| 509 |
+
return log
|
| 510 |
+
|
| 511 |
+
# Step 3: Annotate variants with SnpEff
|
| 512 |
+
log += "## Step 3: Functional Annotation with SnpEff\n"
|
| 513 |
+
annotated_vcf = f"{output_prefix}.annotated.vcf"
|
| 514 |
+
|
| 515 |
+
snpeff_cmd = ["snpEff", "-v", snpeff_database, filtered_vcf, ">", annotated_vcf]
|
| 516 |
+
|
| 517 |
+
log += f"Running command: {' '.join(snpeff_cmd)}\n"
|
| 518 |
+
try:
|
| 519 |
+
# Using shell=True because of the redirection
|
| 520 |
+
subprocess.run(" ".join(snpeff_cmd), shell=True, check=True)
|
| 521 |
+
log += f"Successfully annotated variants: {annotated_vcf}\n\n"
|
| 522 |
+
except subprocess.CalledProcessError as e:
|
| 523 |
+
log += f"Error annotating variants: {str(e)}\n"
|
| 524 |
+
return log
|
| 525 |
+
|
| 526 |
+
# Step 4: Generate summary statistics
|
| 527 |
+
log += "## Step 4: Generating Summary Statistics\n"
|
| 528 |
+
summary_file = f"{output_prefix}_mutation_summary.txt"
|
| 529 |
+
|
| 530 |
+
# Count total variants
|
| 531 |
+
count_cmd = f"grep -v '^#' {annotated_vcf} | wc -l"
|
| 532 |
+
try:
|
| 533 |
+
total_variants = subprocess.check_output(count_cmd, shell=True).decode().strip()
|
| 534 |
+
log += f"Total somatic variants detected: {total_variants}\n"
|
| 535 |
+
except subprocess.CalledProcessError as e:
|
| 536 |
+
log += f"Error counting variants: {str(e)}\n"
|
| 537 |
+
|
| 538 |
+
# Count by variant type
|
| 539 |
+
log += "Variant types:\n"
|
| 540 |
+
for variant_type in ["SNP", "INS", "DEL"]:
|
| 541 |
+
count_type_cmd = f"grep -v '^#' {annotated_vcf} | grep '{variant_type}' | wc -l"
|
| 542 |
+
try:
|
| 543 |
+
type_count = subprocess.check_output(count_type_cmd, shell=True).decode().strip()
|
| 544 |
+
log += f"- {variant_type}: {type_count}\n"
|
| 545 |
+
except subprocess.CalledProcessError:
|
| 546 |
+
log += f"- {variant_type}: Error counting\n"
|
| 547 |
+
|
| 548 |
+
# Count high impact variants
|
| 549 |
+
high_impact_cmd = f"grep -v '^#' {annotated_vcf} | grep 'HIGH' | wc -l"
|
| 550 |
+
try:
|
| 551 |
+
high_impact = subprocess.check_output(high_impact_cmd, shell=True).decode().strip()
|
| 552 |
+
log += f"High impact variants: {high_impact}\n\n"
|
| 553 |
+
except subprocess.CalledProcessError:
|
| 554 |
+
log += "High impact variants: Error counting\n\n"
|
| 555 |
+
|
| 556 |
+
# Step 5: Save summary to file
|
| 557 |
+
with open(summary_file, "w") as f:
|
| 558 |
+
f.write("Somatic Mutation Analysis Summary\n")
|
| 559 |
+
f.write(f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
| 560 |
+
f.write(f"Total somatic variants: {total_variants}\n")
|
| 561 |
+
f.write(f"Output VCF file: {annotated_vcf}\n")
|
| 562 |
+
|
| 563 |
+
log += "## Results\n"
|
| 564 |
+
log += "Analysis complete. Results saved to:\n"
|
| 565 |
+
log += f"- Annotated VCF: {annotated_vcf}\n"
|
| 566 |
+
log += f"- Summary file: {summary_file}\n\n"
|
| 567 |
+
|
| 568 |
+
return log
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def detect_and_characterize_structural_variations(
|
| 572 |
+
bam_file_path,
|
| 573 |
+
reference_genome_path,
|
| 574 |
+
output_dir,
|
| 575 |
+
cosmic_db_path=None,
|
| 576 |
+
clinvar_db_path=None,
|
| 577 |
+
):
|
| 578 |
+
"""Detects and characterizes structural variations (SVs) in genomic sequencing data.
|
| 579 |
+
|
| 580 |
+
This function uses LUMPY for SV detection followed by annotation with COSMIC and/or ClinVar
|
| 581 |
+
databases to identify and characterize various types of structural variations including
|
| 582 |
+
deletions, inversions, translocations, and duplications.
|
| 583 |
+
|
| 584 |
+
Parameters
|
| 585 |
+
----------
|
| 586 |
+
bam_file_path : str
|
| 587 |
+
Path to the aligned sequencing data in BAM format
|
| 588 |
+
reference_genome_path : str
|
| 589 |
+
Path to the reference genome in FASTA format
|
| 590 |
+
output_dir : str
|
| 591 |
+
Directory where results will be saved
|
| 592 |
+
cosmic_db_path : str, optional
|
| 593 |
+
Path to the COSMIC database for cancer annotation
|
| 594 |
+
clinvar_db_path : str, optional
|
| 595 |
+
Path to the ClinVar database for clinical annotation
|
| 596 |
+
|
| 597 |
+
Returns
|
| 598 |
+
-------
|
| 599 |
+
str
|
| 600 |
+
A research log summarizing the steps performed and results obtained
|
| 601 |
+
|
| 602 |
+
"""
|
| 603 |
+
import datetime
|
| 604 |
+
import os
|
| 605 |
+
import subprocess
|
| 606 |
+
|
| 607 |
+
# Create output directory if it doesn't exist
|
| 608 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 609 |
+
|
| 610 |
+
# Initialize research log
|
| 611 |
+
log = []
|
| 612 |
+
log.append("## Structural Variation Detection and Characterization")
|
| 613 |
+
log.append(f"Started at: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 614 |
+
log.append(f"Input BAM file: {bam_file_path}")
|
| 615 |
+
log.append(f"Reference genome: {reference_genome_path}")
|
| 616 |
+
log.append(f"Output directory: {output_dir}")
|
| 617 |
+
log.append("\n")
|
| 618 |
+
|
| 619 |
+
# Step 1: Extract discordant read-pairs and split-reads for LUMPY
|
| 620 |
+
log.append("### Step 1: Extracting discordant read-pairs and split-reads")
|
| 621 |
+
|
| 622 |
+
discordant_bam = os.path.join(output_dir, "discordant.bam")
|
| 623 |
+
split_bam = os.path.join(output_dir, "split.bam")
|
| 624 |
+
|
| 625 |
+
try:
|
| 626 |
+
# Extract discordant read pairs
|
| 627 |
+
subprocess.run(
|
| 628 |
+
[
|
| 629 |
+
"samtools",
|
| 630 |
+
"view",
|
| 631 |
+
"-b",
|
| 632 |
+
"-F",
|
| 633 |
+
"1294",
|
| 634 |
+
"-o",
|
| 635 |
+
discordant_bam,
|
| 636 |
+
bam_file_path,
|
| 637 |
+
],
|
| 638 |
+
check=True,
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
# Extract split reads
|
| 642 |
+
subprocess.run(
|
| 643 |
+
[
|
| 644 |
+
"samtools",
|
| 645 |
+
"view",
|
| 646 |
+
"-h",
|
| 647 |
+
bam_file_path,
|
| 648 |
+
"|",
|
| 649 |
+
"extractSplitReads_BwaMem",
|
| 650 |
+
"-i",
|
| 651 |
+
"stdin",
|
| 652 |
+
"|",
|
| 653 |
+
"samtools",
|
| 654 |
+
"view",
|
| 655 |
+
"-Sb",
|
| 656 |
+
"-",
|
| 657 |
+
">",
|
| 658 |
+
split_bam,
|
| 659 |
+
],
|
| 660 |
+
shell=True,
|
| 661 |
+
check=True,
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
log.append("Successfully extracted discordant read-pairs and split-reads")
|
| 665 |
+
except subprocess.CalledProcessError as e:
|
| 666 |
+
log.append(f"Error during read extraction: {e}")
|
| 667 |
+
return "\n".join(log)
|
| 668 |
+
|
| 669 |
+
# Step 2: Run LUMPY for SV detection
|
| 670 |
+
log.append("\n### Step 2: Running LUMPY for structural variation detection")
|
| 671 |
+
|
| 672 |
+
vcf_output = os.path.join(output_dir, "structural_variants.vcf")
|
| 673 |
+
|
| 674 |
+
try:
|
| 675 |
+
subprocess.run(
|
| 676 |
+
[
|
| 677 |
+
"lumpyexpress",
|
| 678 |
+
"-B",
|
| 679 |
+
bam_file_path,
|
| 680 |
+
"-S",
|
| 681 |
+
split_bam,
|
| 682 |
+
"-D",
|
| 683 |
+
discordant_bam,
|
| 684 |
+
"-o",
|
| 685 |
+
vcf_output,
|
| 686 |
+
],
|
| 687 |
+
check=True,
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
log.append("LUMPY analysis completed successfully")
|
| 691 |
+
log.append(f"Raw SV calls saved to: {vcf_output}")
|
| 692 |
+
except subprocess.CalledProcessError as e:
|
| 693 |
+
log.append(f"Error during LUMPY execution: {e}")
|
| 694 |
+
return "\n".join(log)
|
| 695 |
+
|
| 696 |
+
# Step 3: Filter SVs by quality and size
|
| 697 |
+
log.append("\n### Step 3: Filtering structural variants by quality and size")
|
| 698 |
+
|
| 699 |
+
filtered_vcf = os.path.join(output_dir, "filtered_structural_variants.vcf")
|
| 700 |
+
|
| 701 |
+
try:
|
| 702 |
+
# Filter SVs with quality score >= 100 and size >= 100bp
|
| 703 |
+
subprocess.run(
|
| 704 |
+
[
|
| 705 |
+
"bcftools",
|
| 706 |
+
"filter",
|
| 707 |
+
"-i",
|
| 708 |
+
"QUAL>=100 && SVLEN>=100",
|
| 709 |
+
"-o",
|
| 710 |
+
filtered_vcf,
|
| 711 |
+
vcf_output,
|
| 712 |
+
],
|
| 713 |
+
check=True,
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
# Count SVs by type
|
| 717 |
+
sv_counts = {}
|
| 718 |
+
sv_types = ["DEL", "DUP", "INV", "BND", "INS"]
|
| 719 |
+
|
| 720 |
+
for sv_type in sv_types:
|
| 721 |
+
result = subprocess.run(
|
| 722 |
+
["grep", "-c", f"SVTYPE={sv_type}", filtered_vcf],
|
| 723 |
+
check=False,
|
| 724 |
+
capture_output=True,
|
| 725 |
+
text=True,
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
+
count = 0
|
| 729 |
+
if result.returncode == 0:
|
| 730 |
+
count = int(result.stdout.strip())
|
| 731 |
+
sv_counts[sv_type] = count
|
| 732 |
+
|
| 733 |
+
log.append("SV filtering completed")
|
| 734 |
+
log.append(f"Filtered SVs saved to: {filtered_vcf}")
|
| 735 |
+
log.append("\nSV counts by type:")
|
| 736 |
+
for sv_type, count in sv_counts.items():
|
| 737 |
+
log.append(f"- {sv_type}: {count}")
|
| 738 |
+
except subprocess.CalledProcessError as e:
|
| 739 |
+
log.append(f"Error during SV filtering: {e}")
|
| 740 |
+
return "\n".join(log)
|
| 741 |
+
|
| 742 |
+
# Step 4: Annotate SVs with COSMIC and ClinVar (if databases provided)
|
| 743 |
+
log.append("\n### Step 4: Annotating structural variants")
|
| 744 |
+
|
| 745 |
+
annotated_vcf = os.path.join(output_dir, "annotated_structural_variants.vcf")
|
| 746 |
+
|
| 747 |
+
try:
|
| 748 |
+
annotation_cmd = ["annotate_sv.py", "-i", filtered_vcf, "-o", annotated_vcf]
|
| 749 |
+
|
| 750 |
+
if cosmic_db_path:
|
| 751 |
+
annotation_cmd.extend(["-c", cosmic_db_path])
|
| 752 |
+
log.append(f"Using COSMIC database: {cosmic_db_path}")
|
| 753 |
+
|
| 754 |
+
if clinvar_db_path:
|
| 755 |
+
annotation_cmd.extend(["-v", clinvar_db_path])
|
| 756 |
+
log.append(f"Using ClinVar database: {clinvar_db_path}")
|
| 757 |
+
|
| 758 |
+
# This is a placeholder for the annotation command
|
| 759 |
+
# In a real implementation, you would use a tool like AnnotSV, VEP, or a custom script
|
| 760 |
+
log.append("Note: Annotation step is simulated in this implementation")
|
| 761 |
+
log.append("In a real scenario, tools like AnnotSV or VEP would be used")
|
| 762 |
+
|
| 763 |
+
# Instead of running the command, we'll create a simple annotated file
|
| 764 |
+
with open(filtered_vcf) as infile, open(annotated_vcf, "w") as outfile:
|
| 765 |
+
for line in infile:
|
| 766 |
+
if line.startswith("#"):
|
| 767 |
+
outfile.write(line)
|
| 768 |
+
else:
|
| 769 |
+
outfile.write(line)
|
| 770 |
+
# In a real implementation, annotation would be added here
|
| 771 |
+
|
| 772 |
+
log.append("SV annotation completed")
|
| 773 |
+
log.append(f"Annotated SVs saved to: {annotated_vcf}")
|
| 774 |
+
except Exception as e:
|
| 775 |
+
log.append(f"Error during SV annotation: {e}")
|
| 776 |
+
return "\n".join(log)
|
| 777 |
+
|
| 778 |
+
# Step 5: Generate summary report
|
| 779 |
+
log.append("\n### Step 5: Generating summary report")
|
| 780 |
+
|
| 781 |
+
summary_file = os.path.join(output_dir, "sv_summary_report.tsv")
|
| 782 |
+
|
| 783 |
+
try:
|
| 784 |
+
# Convert VCF to a tabular format for easier analysis
|
| 785 |
+
subprocess.run(
|
| 786 |
+
[
|
| 787 |
+
"bcftools",
|
| 788 |
+
"query",
|
| 789 |
+
"-f",
|
| 790 |
+
"%CHROM\t%POS\t%INFO/SVTYPE\t%INFO/SVLEN\t%QUAL\n",
|
| 791 |
+
"-o",
|
| 792 |
+
summary_file,
|
| 793 |
+
annotated_vcf,
|
| 794 |
+
],
|
| 795 |
+
check=True,
|
| 796 |
+
)
|
| 797 |
+
|
| 798 |
+
log.append(f"Summary report generated: {summary_file}")
|
| 799 |
+
except subprocess.CalledProcessError as e:
|
| 800 |
+
log.append(f"Error generating summary report: {e}")
|
| 801 |
+
return "\n".join(log)
|
| 802 |
+
|
| 803 |
+
# Final summary
|
| 804 |
+
log.append("\n## Summary")
|
| 805 |
+
log.append(f"Total SVs detected: {sum(sv_counts.values())}")
|
| 806 |
+
log.append("SV types breakdown:")
|
| 807 |
+
for sv_type, count in sv_counts.items():
|
| 808 |
+
log.append(f"- {sv_type}: {count} ({count / sum(sv_counts.values()) * 100:.1f}%)")
|
| 809 |
+
|
| 810 |
+
log.append("\nFiles generated:")
|
| 811 |
+
log.append(f"- Raw SV calls: {vcf_output}")
|
| 812 |
+
log.append(f"- Filtered SVs: {filtered_vcf}")
|
| 813 |
+
log.append(f"- Annotated SVs: {annotated_vcf}")
|
| 814 |
+
log.append(f"- Summary report: {summary_file}")
|
| 815 |
+
|
| 816 |
+
log.append(f"\nAnalysis completed at: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 817 |
+
|
| 818 |
+
return "\n".join(log)
|
| 819 |
+
|
| 820 |
+
|
| 821 |
+
def perform_gene_expression_nmf_analysis(
|
| 822 |
+
expression_data_path,
|
| 823 |
+
n_components=10,
|
| 824 |
+
normalize=True,
|
| 825 |
+
output_dir="nmf_results",
|
| 826 |
+
random_state=42,
|
| 827 |
+
):
|
| 828 |
+
"""Performs Non-negative Matrix Factorization (NMF) on gene expression data to extract
|
| 829 |
+
metagenes and their associated sample weights for tumor subtype identification.
|
| 830 |
+
|
| 831 |
+
Parameters
|
| 832 |
+
----------
|
| 833 |
+
expression_data_path : str
|
| 834 |
+
Path to a CSV or TSV file containing gene expression data with genes as rows and samples as columns.
|
| 835 |
+
Values should be non-negative (e.g., normalized counts or expression values).
|
| 836 |
+
n_components : int, default=10
|
| 837 |
+
Number of metagenes (components) to extract.
|
| 838 |
+
normalize : bool, default=True
|
| 839 |
+
Whether to normalize the expression data before applying NMF.
|
| 840 |
+
output_dir : str, default="nmf_results"
|
| 841 |
+
Directory to save the output files.
|
| 842 |
+
random_state : int, default=42
|
| 843 |
+
Random seed for reproducibility.
|
| 844 |
+
|
| 845 |
+
Returns
|
| 846 |
+
-------
|
| 847 |
+
str
|
| 848 |
+
Research log summarizing the analysis steps and results.
|
| 849 |
+
|
| 850 |
+
"""
|
| 851 |
+
import os
|
| 852 |
+
from datetime import datetime
|
| 853 |
+
|
| 854 |
+
import numpy as np
|
| 855 |
+
import pandas as pd
|
| 856 |
+
from sklearn.decomposition import NMF
|
| 857 |
+
|
| 858 |
+
# Start research log
|
| 859 |
+
log = []
|
| 860 |
+
log.append(f"NMF GENE EXPRESSION ANALYSIS - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 861 |
+
log.append(f"Parameters: n_components={n_components}, normalize={normalize}")
|
| 862 |
+
|
| 863 |
+
# Load expression data from file
|
| 864 |
+
try:
|
| 865 |
+
if expression_data_path.endswith(".csv"):
|
| 866 |
+
expression_data = pd.read_csv(expression_data_path, index_col=0)
|
| 867 |
+
elif expression_data_path.endswith((".tsv", ".txt")):
|
| 868 |
+
expression_data = pd.read_csv(expression_data_path, sep="\t", index_col=0)
|
| 869 |
+
else:
|
| 870 |
+
log.append("ERROR: Unsupported file format. Please provide a CSV or TSV file.")
|
| 871 |
+
return "\n".join(log)
|
| 872 |
+
|
| 873 |
+
log.append(f"Successfully loaded expression data from {expression_data_path}")
|
| 874 |
+
except Exception as e:
|
| 875 |
+
log.append(f"ERROR: Failed to load expression data from {expression_data_path}. Error: {str(e)}")
|
| 876 |
+
return "\n".join(log)
|
| 877 |
+
|
| 878 |
+
# Extract gene and sample information
|
| 879 |
+
genes = expression_data.index.tolist()
|
| 880 |
+
samples = expression_data.columns.tolist()
|
| 881 |
+
X = expression_data.values
|
| 882 |
+
log.append(f"Input data: {len(genes)} genes × {len(samples)} samples")
|
| 883 |
+
|
| 884 |
+
# Check for non-negative values
|
| 885 |
+
if np.any(X < 0):
|
| 886 |
+
log.append("WARNING: Negative values found in expression data. Converting to absolute values.")
|
| 887 |
+
X = np.abs(X)
|
| 888 |
+
|
| 889 |
+
# Normalize data if requested
|
| 890 |
+
if normalize:
|
| 891 |
+
log.append("Normalizing expression data...")
|
| 892 |
+
X = X / np.sum(X, axis=0, keepdims=True) * 1000 # TPM-like normalization
|
| 893 |
+
|
| 894 |
+
# Create output directory if it doesn't exist
|
| 895 |
+
if not os.path.exists(output_dir):
|
| 896 |
+
os.makedirs(output_dir)
|
| 897 |
+
log.append(f"Created output directory: {output_dir}")
|
| 898 |
+
|
| 899 |
+
# Apply NMF
|
| 900 |
+
log.append(f"Applying NMF to extract {n_components} metagenes...")
|
| 901 |
+
model = NMF(
|
| 902 |
+
n_components=n_components,
|
| 903 |
+
init="random",
|
| 904 |
+
random_state=random_state,
|
| 905 |
+
max_iter=1000,
|
| 906 |
+
)
|
| 907 |
+
|
| 908 |
+
try:
|
| 909 |
+
# W: metagenes (genes × components)
|
| 910 |
+
# H: sample weights (components × samples)
|
| 911 |
+
W = model.fit_transform(X)
|
| 912 |
+
H = model.components_
|
| 913 |
+
|
| 914 |
+
log.append(f"NMF completed successfully in {model.n_iter_} iterations")
|
| 915 |
+
log.append(f"Explained variance: {model.reconstruction_err_:.4f}")
|
| 916 |
+
|
| 917 |
+
# Save metagenes (W matrix)
|
| 918 |
+
metagenes_file = os.path.join(output_dir, "metagenes.csv")
|
| 919 |
+
pd.DataFrame(W, index=genes, columns=[f"Metagene_{i + 1}" for i in range(n_components)]).to_csv(metagenes_file)
|
| 920 |
+
log.append(f"Saved metagenes to {metagenes_file}")
|
| 921 |
+
|
| 922 |
+
# Save sample weights (H matrix)
|
| 923 |
+
weights_file = os.path.join(output_dir, "sample_weights.csv")
|
| 924 |
+
pd.DataFrame(H, index=[f"Metagene_{i + 1}" for i in range(n_components)], columns=samples).to_csv(weights_file)
|
| 925 |
+
log.append(f"Saved sample weights to {weights_file}")
|
| 926 |
+
|
| 927 |
+
top_genes_file = os.path.join(output_dir, "top_genes_per_metagene.csv")
|
| 928 |
+
top_genes = {}
|
| 929 |
+
for i in range(n_components):
|
| 930 |
+
# Get indices of top 20 genes for this metagene
|
| 931 |
+
top_indices = np.argsort(W[:, i])[::-1][:20]
|
| 932 |
+
top_genes[f"Metagene_{i + 1}"] = [genes[idx] for idx in top_indices]
|
| 933 |
+
|
| 934 |
+
pd.DataFrame(top_genes).to_csv(top_genes_file)
|
| 935 |
+
log.append(f"Saved top genes per metagene to {top_genes_file}")
|
| 936 |
+
|
| 937 |
+
except Exception as e:
|
| 938 |
+
log.append(f"ERROR: NMF failed with error: {str(e)}")
|
| 939 |
+
return "\n".join(log)
|
| 940 |
+
|
| 941 |
+
log.append("\nSUMMARY:")
|
| 942 |
+
log.append(f"- Successfully extracted {n_components} metagenes from {X.shape[0]} genes across {X.shape[1]} samples")
|
| 943 |
+
log.append(f"- Results saved to: {output_dir}")
|
| 944 |
+
log.append("- Files generated: metagenes.csv, sample_weights.csv, top_genes_per_metagene.csv")
|
| 945 |
+
log.append("\nNEXT STEPS:")
|
| 946 |
+
log.append("- Analyze the metagenes to identify biological pathways")
|
| 947 |
+
log.append("- Cluster samples based on metagene weights to identify potential tumor subtypes")
|
| 948 |
+
log.append("- Correlate subtypes with clinical outcomes if available")
|
| 949 |
+
|
| 950 |
+
return "\n".join(log)
|
| 951 |
+
|
| 952 |
+
|
| 953 |
+
def analyze_copy_number_purity_ploidy_and_focal_events(
|
| 954 |
+
tumor_bam,
|
| 955 |
+
reference_genome,
|
| 956 |
+
normal_bam=None,
|
| 957 |
+
output_dir="cn_analysis_results",
|
| 958 |
+
targets_bed=None,
|
| 959 |
+
antitargets_bed=None,
|
| 960 |
+
gene_bed=None,
|
| 961 |
+
focal_genes=None,
|
| 962 |
+
log2_amp_threshold=1.0,
|
| 963 |
+
log2_del_threshold=-1.0,
|
| 964 |
+
):
|
| 965 |
+
"""CNVkit-based copy number workflow: CNV segmentation, purity/ploidy & HRD approximation, focal events.
|
| 966 |
+
|
| 967 |
+
This function orchestrates a CNVkit-based copy number analysis workflow for tumor samples.
|
| 968 |
+
It performs CNV segmentation, derives approximate purity & ploidy metrics, calculates a simplified
|
| 969 |
+
HRD-style summary, and detects focal amplifications/deletions in key oncogenes / tumor suppressors.
|
| 970 |
+
|
| 971 |
+
Parameters
|
| 972 |
+
----------
|
| 973 |
+
tumor_bam : str
|
| 974 |
+
Path to tumor BAM (indexed)
|
| 975 |
+
reference_genome : str
|
| 976 |
+
Path to reference FASTA (with index files present)
|
| 977 |
+
normal_bam : str, optional
|
| 978 |
+
Matched normal BAM (recommended for allele-specific & purity inference)
|
| 979 |
+
output_dir : str, default "cn_analysis_results"
|
| 980 |
+
Directory for all outputs
|
| 981 |
+
targets_bed : str, optional
|
| 982 |
+
BED of target regions (e.g. exome / panel) for CNVkit / gCNV
|
| 983 |
+
antitargets_bed : str, optional
|
| 984 |
+
BED of antitarget regions for CNVkit (if panel / exome)
|
| 985 |
+
gene_bed : str, optional
|
| 986 |
+
BED file with gene coordinates (chrom, start, end, gene). Used for focal event annotation
|
| 987 |
+
focal_genes : list[str], optional
|
| 988 |
+
List of genes to highlight for focal amplification/deletion; defaults to ["MYC","ERBB2","CDKN2A"]
|
| 989 |
+
log2_amp_threshold : float, default 1.0
|
| 990 |
+
Log2 ratio threshold to call focal amplification (~ >2x copy relative to baseline)
|
| 991 |
+
log2_del_threshold : float, default -1.0
|
| 992 |
+
Log2 ratio threshold to call focal deep deletion (~ homozygous loss)
|
| 993 |
+
|
| 994 |
+
Returns
|
| 995 |
+
-------
|
| 996 |
+
str
|
| 997 |
+
Research log summarizing steps, tool executions, and findings.
|
| 998 |
+
"""
|
| 999 |
+
import datetime
|
| 1000 |
+
import math
|
| 1001 |
+
import os
|
| 1002 |
+
import shutil
|
| 1003 |
+
import statistics
|
| 1004 |
+
import subprocess
|
| 1005 |
+
|
| 1006 |
+
import pandas as pd
|
| 1007 |
+
|
| 1008 |
+
log = []
|
| 1009 |
+
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 1010 |
+
log.append(f"CNVKIT COPY NUMBER ANALYSIS WORKFLOW - {ts}")
|
| 1011 |
+
log.append("=" * 80)
|
| 1012 |
+
log.append(f"Tumor BAM: {tumor_bam}")
|
| 1013 |
+
if normal_bam:
|
| 1014 |
+
log.append(f"Normal BAM: {normal_bam}")
|
| 1015 |
+
log.append(f"Reference genome: {reference_genome}")
|
| 1016 |
+
log.append(f"Output directory: {output_dir}\n")
|
| 1017 |
+
|
| 1018 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1019 |
+
sample_name = os.path.splitext(os.path.basename(tumor_bam))[0]
|
| 1020 |
+
|
| 1021 |
+
if focal_genes is None:
|
| 1022 |
+
focal_genes = ["MYC", "ERBB2", "CDKN2A"]
|
| 1023 |
+
# Single informative log line (was duplicated previously)
|
| 1024 |
+
log.append(f"Focal genes for analysis: {', '.join(focal_genes)}")
|
| 1025 |
+
|
| 1026 |
+
# ----------------------------------------------------------------------------------
|
| 1027 |
+
# STEP 1: CNV SEGMENTATION with CNVkit
|
| 1028 |
+
# ----------------------------------------------------------------------------------
|
| 1029 |
+
log.append("STEP 1: CNV Segmentation with CNVkit")
|
| 1030 |
+
cnvkit_path = shutil.which("cnvkit.py") or shutil.which("cnvkit")
|
| 1031 |
+
use_conda_env = False
|
| 1032 |
+
|
| 1033 |
+
# Check if CNVkit is available in biomni_e1 environment
|
| 1034 |
+
if not cnvkit_path:
|
| 1035 |
+
try:
|
| 1036 |
+
result = subprocess.run(
|
| 1037 |
+
["conda", "run", "-n", "biomni_e1", "which", "cnvkit.py"], capture_output=True, text=True, check=True
|
| 1038 |
+
)
|
| 1039 |
+
if result.returncode == 0 and result.stdout.strip():
|
| 1040 |
+
cnvkit_path = "cnvkit.py" # Will use via conda run
|
| 1041 |
+
use_conda_env = True
|
| 1042 |
+
log.append("- CNVkit found in biomni_e1 environment")
|
| 1043 |
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
| 1044 |
+
pass
|
| 1045 |
+
|
| 1046 |
+
# Fallback to bio_env_py310
|
| 1047 |
+
if not cnvkit_path:
|
| 1048 |
+
try:
|
| 1049 |
+
result = subprocess.run(
|
| 1050 |
+
["conda", "run", "-n", "bio_env_py310", "which", "cnvkit.py"],
|
| 1051 |
+
capture_output=True,
|
| 1052 |
+
text=True,
|
| 1053 |
+
check=True,
|
| 1054 |
+
)
|
| 1055 |
+
if result.returncode == 0 and result.stdout.strip():
|
| 1056 |
+
cnvkit_path = "cnvkit.py" # Will use via conda run
|
| 1057 |
+
use_conda_env = True
|
| 1058 |
+
log.append("- CNVkit found in bio_env_py310 environment")
|
| 1059 |
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
| 1060 |
+
pass
|
| 1061 |
+
|
| 1062 |
+
if not cnvkit_path:
|
| 1063 |
+
log.append("- CNVkit not detected in current PATH, biomni_e1, or bio_env_py310 environment")
|
| 1064 |
+
log.append("- Install manually (e.g., pip install cnvkit==0.9.11) or run setup.sh")
|
| 1065 |
+
log.append("- Workflow will exit without CNVkit")
|
| 1066 |
+
return "\n".join(log)
|
| 1067 |
+
|
| 1068 |
+
cnvkit_cns = None
|
| 1069 |
+
log.append(f"- CNVkit detected at: {cnvkit_path}")
|
| 1070 |
+
|
| 1071 |
+
# Determine environment prefix for CNVkit commands
|
| 1072 |
+
env_prefix = ["conda", "run", "-n", "biomni_e1"] if use_conda_env else []
|
| 1073 |
+
|
| 1074 |
+
# CNVkit batch command
|
| 1075 |
+
batch_cmd = env_prefix + [cnvkit_path, "batch", tumor_bam, "-d", output_dir, "-f", reference_genome]
|
| 1076 |
+
if normal_bam:
|
| 1077 |
+
batch_cmd.extend(["-n", normal_bam])
|
| 1078 |
+
if targets_bed:
|
| 1079 |
+
batch_cmd.extend(["-t", targets_bed])
|
| 1080 |
+
if antitargets_bed:
|
| 1081 |
+
batch_cmd.extend(["-a", antitargets_bed])
|
| 1082 |
+
batch_cmd.extend(["--scatter", "--diagram"])
|
| 1083 |
+
|
| 1084 |
+
log.append(f"- Running CNVkit batch command: {' '.join(batch_cmd)}")
|
| 1085 |
+
try:
|
| 1086 |
+
subprocess.run(batch_cmd, check=True, capture_output=True)
|
| 1087 |
+
log.append("- CNVkit batch completed successfully")
|
| 1088 |
+
except subprocess.CalledProcessError as e:
|
| 1089 |
+
log.append(f"- WARNING: CNVkit batch failed: {e}. Proceeding with limited downstream analyses")
|
| 1090 |
+
|
| 1091 |
+
# Check for CNVkit output and run call command
|
| 1092 |
+
cnvkit_cns = os.path.join(output_dir, f"{sample_name}.cns")
|
| 1093 |
+
if os.path.exists(cnvkit_cns):
|
| 1094 |
+
call_cmd = env_prefix + [
|
| 1095 |
+
cnvkit_path,
|
| 1096 |
+
"call",
|
| 1097 |
+
cnvkit_cns,
|
| 1098 |
+
"-o",
|
| 1099 |
+
os.path.join(output_dir, f"{sample_name}.call.cns"),
|
| 1100 |
+
"-m",
|
| 1101 |
+
"clonal",
|
| 1102 |
+
]
|
| 1103 |
+
log.append(f"- Calling absolute CN: {' '.join(call_cmd)}")
|
| 1104 |
+
try:
|
| 1105 |
+
subprocess.run(call_cmd, check=True, capture_output=True)
|
| 1106 |
+
cnvkit_cns = os.path.join(output_dir, f"{sample_name}.call.cns")
|
| 1107 |
+
log.append("- CNVkit call succeeded")
|
| 1108 |
+
except subprocess.CalledProcessError as e:
|
| 1109 |
+
log.append(f"- WARNING: CNVkit call failed: {e}")
|
| 1110 |
+
else:
|
| 1111 |
+
log.append("- WARNING: Expected CNVkit .cns file not found")
|
| 1112 |
+
|
| 1113 |
+
# ----------------------------------------------------------------------------------
|
| 1114 |
+
# STEP 2: Purity & ploidy approximation (based on CNVkit outputs)
|
| 1115 |
+
# ----------------------------------------------------------------------------------
|
| 1116 |
+
log.append("\nSTEP 2: Purity & Ploidy Approximation")
|
| 1117 |
+
purity_est = None
|
| 1118 |
+
ploidy_est = None
|
| 1119 |
+
seg_source = None
|
| 1120 |
+
seg_df = None
|
| 1121 |
+
try:
|
| 1122 |
+
if cnvkit_cns and os.path.exists(cnvkit_cns):
|
| 1123 |
+
seg_source = cnvkit_cns
|
| 1124 |
+
seg_df = pd.read_csv(cnvkit_cns, sep="\t")
|
| 1125 |
+
log.append(f"- Using CNVkit segments from: {seg_source}")
|
| 1126 |
+
|
| 1127 |
+
# Harmonize columns
|
| 1128 |
+
cols = [c.lower() for c in seg_df.columns]
|
| 1129 |
+
seg_df.columns = cols
|
| 1130 |
+
|
| 1131 |
+
# Obtain log2 column
|
| 1132 |
+
log2_col = None
|
| 1133 |
+
for candidate in ["log2", "log2ratio", "cnlr", "logr"]:
|
| 1134 |
+
if candidate in cols:
|
| 1135 |
+
log2_col = candidate
|
| 1136 |
+
break
|
| 1137 |
+
for candidate in ["end", "chromend", "stop"]:
|
| 1138 |
+
if candidate in cols:
|
| 1139 |
+
end_col = candidate
|
| 1140 |
+
break
|
| 1141 |
+
for candidate in ["start", "chromstart", "begin"]:
|
| 1142 |
+
if candidate in cols:
|
| 1143 |
+
start_col = candidate
|
| 1144 |
+
break
|
| 1145 |
+
|
| 1146 |
+
if log2_col and start_col and end_col:
|
| 1147 |
+
seg_df["seg_length"] = seg_df[end_col] - seg_df[start_col]
|
| 1148 |
+
# Weighted average absolute copy ratio -> approximate ploidy baseline
|
| 1149 |
+
# Convert log2 ratio to absolute copy ratio (assuming diploid baseline = 2)
|
| 1150 |
+
seg_df["abs_cn"] = 2 * (2 ** seg_df[log2_col])
|
| 1151 |
+
weighted_mean_cn = (seg_df["abs_cn"] * seg_df["seg_length"]).sum() / seg_df["seg_length"].sum()
|
| 1152 |
+
ploidy_est = weighted_mean_cn
|
| 1153 |
+
# Approximate purity: higher variance in log2 ratios suggests purity; naive formula
|
| 1154 |
+
mad_log2 = statistics.median([abs(x) for x in seg_df[log2_col] if not math.isnan(x)])
|
| 1155 |
+
purity_est = min(1.0, max(0.2, 1 - mad_log2 / 1.5)) # heuristic clamp
|
| 1156 |
+
log.append(f"- Estimated ploidy (weighted mean CN): {ploidy_est:.2f}")
|
| 1157 |
+
log.append(f"- Estimated purity (heuristic from segmentation dispersion): {purity_est:.2f}")
|
| 1158 |
+
else:
|
| 1159 |
+
log.append("- WARNING: Could not identify necessary columns for purity/ploidy estimation")
|
| 1160 |
+
else:
|
| 1161 |
+
log.append("- No CNVkit segmentation available for purity/ploidy estimation")
|
| 1162 |
+
except Exception as e:
|
| 1163 |
+
log.append(f"- ERROR: Purity/ploidy estimation failed: {e}")
|
| 1164 |
+
|
| 1165 |
+
# ----------------------------------------------------------------------------------
|
| 1166 |
+
# STEP 3: HRD-like summary (simplified proxy)
|
| 1167 |
+
# ----------------------------------------------------------------------------------
|
| 1168 |
+
log.append("\nSTEP 3: HRD Approximation (Simplified)")
|
| 1169 |
+
hrd_metrics = {}
|
| 1170 |
+
try:
|
| 1171 |
+
if seg_df is not None and "seg_length" in seg_df:
|
| 1172 |
+
# Count large-scale transitions (LST proxy: adjacent segments >10 Mb with log2 change >0.2)
|
| 1173 |
+
seg_df_sorted = seg_df.sort_values(
|
| 1174 |
+
by=[c for c in seg_df.columns if c.startswith("chrom") or c == "chrom" or c == "chromosome"][0]
|
| 1175 |
+
if any(c.startswith("chrom") for c in seg_df.columns)
|
| 1176 |
+
else seg_df.columns[0]
|
| 1177 |
+
)
|
| 1178 |
+
lst_count = 0
|
| 1179 |
+
prev = None
|
| 1180 |
+
for _, row in seg_df_sorted.iterrows():
|
| 1181 |
+
if prev is not None:
|
| 1182 |
+
if row["seg_length"] >= 10_000_000 and prev["seg_length"] >= 10_000_000:
|
| 1183 |
+
if log2_col and abs(row[log2_col] - prev[log2_col]) > 0.2:
|
| 1184 |
+
lst_count += 1
|
| 1185 |
+
prev = row
|
| 1186 |
+
hrd_metrics["LST_like"] = lst_count
|
| 1187 |
+
# HRD-LOH proxy: segments with log2 < -0.3 and length >15 Mb
|
| 1188 |
+
if log2_col:
|
| 1189 |
+
hrd_loh = ((seg_df[log2_col] < -0.3) & (seg_df["seg_length"] > 15_000_000)).sum()
|
| 1190 |
+
hrd_metrics["HRD_LOH_like"] = int(hrd_loh)
|
| 1191 |
+
# Telomeric AI proxy omitted (needs allele-specific data)
|
| 1192 |
+
hrd_score = sum(hrd_metrics.values())
|
| 1193 |
+
hrd_metrics["Simplified_HRD_score"] = hrd_score
|
| 1194 |
+
log.append(f"- LST-like events: {hrd_metrics['LST_like']}")
|
| 1195 |
+
log.append(f"- HRD-LOH-like events: {hrd_metrics['HRD_LOH_like']}")
|
| 1196 |
+
log.append(f"- Simplified composite HRD score: {hrd_metrics['Simplified_HRD_score']}")
|
| 1197 |
+
log.append("- NOTE: For clinical/research use, apply scarHRD or HRDetect with allele-specific data")
|
| 1198 |
+
else:
|
| 1199 |
+
log.append("- Segmentation not available; HRD approximation skipped")
|
| 1200 |
+
except Exception as e:
|
| 1201 |
+
log.append(f"- ERROR: HRD approximation failed: {e}")
|
| 1202 |
+
|
| 1203 |
+
# ----------------------------------------------------------------------------------
|
| 1204 |
+
# STEP 4: Focal amplifications/deletions in key genes
|
| 1205 |
+
# ----------------------------------------------------------------------------------
|
| 1206 |
+
log.append("\nSTEP 4: Focal Amplifications / Deletions in Key Genes")
|
| 1207 |
+
focal_events = []
|
| 1208 |
+
try:
|
| 1209 |
+
if seg_df is not None and gene_bed and os.path.exists(gene_bed):
|
| 1210 |
+
genes_df = pd.read_csv(gene_bed, sep="\t", header=None, names=["chrom", "start", "end", "gene"])
|
| 1211 |
+
|
| 1212 |
+
# Normalize chromosome naming
|
| 1213 |
+
def norm_chr(x):
|
| 1214 |
+
return x.replace("chr", "") if isinstance(x, str) else x
|
| 1215 |
+
|
| 1216 |
+
seg_df["chr_norm"] = (
|
| 1217 |
+
seg_df[[c for c in seg_df.columns if c in ["chrom", "chromosome", "chr"]][0]].astype(str).map(norm_chr)
|
| 1218 |
+
)
|
| 1219 |
+
genes_df["chr_norm"] = genes_df["chrom"].astype(str).map(norm_chr)
|
| 1220 |
+
for gene in focal_genes:
|
| 1221 |
+
region = genes_df[genes_df["gene"] == gene]
|
| 1222 |
+
if region.empty:
|
| 1223 |
+
continue
|
| 1224 |
+
r = region.iloc[0]
|
| 1225 |
+
# Ensure we work on a copy to avoid SettingWithCopyWarning
|
| 1226 |
+
overlaps = seg_df[
|
| 1227 |
+
(seg_df["chr_norm"] == r["chr_norm"])
|
| 1228 |
+
& (seg_df[start_col] <= r["end"])
|
| 1229 |
+
& (seg_df[end_col] >= r["start"])
|
| 1230 |
+
].copy()
|
| 1231 |
+
if not overlaps.empty and log2_col:
|
| 1232 |
+
# Choose segment with largest overlap length
|
| 1233 |
+
r_end = r["end"]
|
| 1234 |
+
r_start = r["start"]
|
| 1235 |
+
overlaps["ov_len"] = overlaps.apply(
|
| 1236 |
+
lambda row, r_end=r_end, r_start=r_start: min(row[end_col], r_end)
|
| 1237 |
+
- max(row[start_col], r_start),
|
| 1238 |
+
axis=1,
|
| 1239 |
+
)
|
| 1240 |
+
best = overlaps.sort_values("ov_len", ascending=False).iloc[0]
|
| 1241 |
+
l2 = best[log2_col]
|
| 1242 |
+
status = None
|
| 1243 |
+
if l2 >= log2_amp_threshold:
|
| 1244 |
+
status = "AMPLIFICATION"
|
| 1245 |
+
elif l2 <= log2_del_threshold:
|
| 1246 |
+
status = "DELETION"
|
| 1247 |
+
if status:
|
| 1248 |
+
focal_events.append((gene, status, l2))
|
| 1249 |
+
if focal_events:
|
| 1250 |
+
for gene, status, l2 in focal_events:
|
| 1251 |
+
log.append(f"- {gene}: {status} (log2={l2:.2f})")
|
| 1252 |
+
else:
|
| 1253 |
+
log.append("- No focal events meeting thresholds in target genes")
|
| 1254 |
+
else:
|
| 1255 |
+
if not gene_bed:
|
| 1256 |
+
log.append("- Skipped: gene_bed not provided")
|
| 1257 |
+
else:
|
| 1258 |
+
log.append("- Skipped: gene_bed file not found or segmentation unavailable")
|
| 1259 |
+
except Exception as e:
|
| 1260 |
+
log.append(f"- ERROR: Focal event detection failed: {e}")
|
| 1261 |
+
|
| 1262 |
+
# ----------------------------------------------------------------------------------
|
| 1263 |
+
# STEP 5: Summaries & file outputs
|
| 1264 |
+
# ----------------------------------------------------------------------------------
|
| 1265 |
+
log.append("\nSTEP 5: Summary & Output Files")
|
| 1266 |
+
summary = {
|
| 1267 |
+
"purity_estimate": purity_est,
|
| 1268 |
+
"ploidy_estimate": ploidy_est,
|
| 1269 |
+
}
|
| 1270 |
+
summary.update(hrd_metrics)
|
| 1271 |
+
summary_file = os.path.join(output_dir, f"{sample_name}_cn_summary.tsv")
|
| 1272 |
+
try:
|
| 1273 |
+
pd.DataFrame([summary]).to_csv(summary_file, sep="\t", index=False)
|
| 1274 |
+
log.append(f"- Saved summary metrics: {summary_file}")
|
| 1275 |
+
except Exception as e:
|
| 1276 |
+
log.append(f"- WARNING: Could not save summary file: {e}")
|
| 1277 |
+
|
| 1278 |
+
if focal_events:
|
| 1279 |
+
focal_file = os.path.join(output_dir, f"{sample_name}_focal_events.tsv")
|
| 1280 |
+
try:
|
| 1281 |
+
pd.DataFrame(focal_events, columns=["Gene", "Event", "Log2"]).to_csv(focal_file, sep="\t", index=False)
|
| 1282 |
+
log.append(f"- Saved focal events: {focal_file}")
|
| 1283 |
+
except Exception as e:
|
| 1284 |
+
log.append(f"- WARNING: Could not save focal events file: {e}")
|
| 1285 |
+
|
| 1286 |
+
log.append("\nNEXT STEPS:")
|
| 1287 |
+
log.append("- For improved purity/ploidy estimation, consider ABSOLUTE, FACETS, PureCN, or Sequenza")
|
| 1288 |
+
log.append("- For validated HRD scoring, use scarHRD or HRDetect with allele-specific data")
|
| 1289 |
+
log.append("- Integrate CN events with expression data to assess driver gene activation/dosage")
|
| 1290 |
+
log.append("- Visualize CNV profiles using CNVkit's built-in plotting functions")
|
| 1291 |
+
|
| 1292 |
+
return "\n".join(log)
|
Biomanus_upload/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)
|
Biomanus_upload/biomni/tool/database.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|