czty commited on
Commit
4c8cb4b
·
verified ·
1 Parent(s): a4fc8f3

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. Biomni/.env.example +22 -0
  2. Biomni/.gitignore +234 -0
  3. Biomni/.pre-commit-config.yaml +41 -0
  4. Biomni/DETAILS.md +364 -0
  5. Biomni/LICENSE +201 -0
  6. Biomni/MANIFEST.in +27 -0
  7. Biomni/Q13398_ChIP-seq_Meta-clusters.bb +0 -0
  8. Biomni/biomni/__init__.py +3 -0
  9. Biomni/biomni/agent/__init__.py +1 -0
  10. Biomni/biomni/agent/__pycache__/__init__.cpython-311.pyc +0 -0
  11. Biomni/biomni/agent/a1.py +0 -0
  12. Biomni/biomni/agent/env_collection.py +313 -0
  13. Biomni/biomni/agent/function_generator.py +119 -0
  14. Biomni/biomni/agent/qa_llm.py +50 -0
  15. Biomni/biomni/agent/react.py +465 -0
  16. Biomni/biomni/config.py +99 -0
  17. Biomni/biomni/env_desc.py +221 -0
  18. Biomni/biomni/env_desc_cm.py +219 -0
  19. Biomni/biomni/eval/__init__.py +3 -0
  20. Biomni/biomni/llm.py +275 -0
  21. Biomni/biomni/task/__init__.py +0 -0
  22. Biomni/biomni/task/base_task.py +18 -0
  23. Biomni/biomni/task/hle.py +146 -0
  24. Biomni/biomni/task/lab_bench.py +115 -0
  25. Biomni/biomni/tool/__init__.py +1 -0
  26. Biomni/biomni/tool/biochemistry.py +1026 -0
  27. Biomni/biomni/tool/bioimaging.py +1394 -0
  28. Biomni/biomni/tool/biophysics.py +513 -0
  29. Biomni/biomni/tool/cell_biology.py +742 -0
  30. Biomni/biomni/tool/database.py +0 -0
  31. Biomni/biomni/tool/example_mcp_tools/pubmed_mcp.py +38 -0
  32. Biomni/biomni/tool/genetics.py +1672 -0
  33. Biomni/biomni/tool/literature.py +403 -0
  34. Biomni/biomni/tool/pathology.py +1034 -0
  35. Biomni/biomni/tool/protocols/thermofisher/Cell Freezing Protocols _ Thermo Fisher Scientific - US.txt +105 -0
  36. Biomni/biomni/tool/synthetic_biology.py +1351 -0
  37. Biomni/biomni/tool/systems_biology.py +926 -0
  38. Biomni/biomni/tool/tool_registry.py +89 -0
  39. Biomni/biomni/utils.py +2469 -0
  40. Biomni/biomni/version.py +1 -0
  41. Biomni/license_info.md +52 -0
  42. Biomni/pyproject.toml +105 -0
  43. cloningscenarios_all_models_cloning_20260524_163903.log +7 -0
  44. ollama.log +13 -0
  45. rerun_base_deepseek_cloningscenarios_50shards.py +153 -0
  46. rerun_base_deepseek_cloningscenarios_50shards.sh +80 -0
  47. run_base_openai_labbench_three_tasks_50shards.sh +127 -0
  48. run_biomni_nomcp_cloningscenarios.sh +108 -0
  49. run_cloningscenarios_all_models.sh +232 -0
  50. run_cloningscenarios_reactcode_and_base_50shards.sh +132 -0
Biomni/.env.example ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Biomni Environment Configuration
2
+ # Copy this file to .env and fill in your actual API keys
3
+
4
+ # Required: Anthropic API Key for Claude models
5
+ ANTHROPIC_API_KEY=your_anthropic_api_key_here
6
+
7
+ # Optional: OpenAI API Key (if using OpenAI models)
8
+ OPENAI_API_KEY=your_openai_api_key_here
9
+
10
+ # Optional: AWS Bedrock Configuration (if using AWS Bedrock models)
11
+ AWS_BEARER_TOKEN_BEDROCK=your_bedrock_api_key_here
12
+ AWS_REGION=us-east-1
13
+
14
+ # Optional: Custom model serving configuration
15
+ # CUSTOM_MODEL_BASE_URL=http://localhost:8000/v1
16
+ # CUSTOM_MODEL_API_KEY=your_custom_api_key_here
17
+
18
+ # Optional: Biomni data path (defaults to ./data)
19
+ # BIOMNI_DATA_PATH=/path/to/your/data
20
+
21
+ # Optional: Timeout settings
22
+ # BIOMNI_TIMEOUT_SECONDS=600
Biomni/.gitignore ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .gitignore
2
+ .buildplan
3
+ .env
4
+ debug*
5
+ .*
6
+ # Byte-compiled / optimized / DLL files
7
+ biomni_release/biomni_env/biomni_tools/
8
+ open_source_process.ipynb
9
+ biomni_env/biomni_tools/*
10
+ .gradio/
11
+ *xlsx
12
+ scripts/*.png
13
+ notebooks/figures/*
14
+ notebooks/tmp_dir/*
15
+ notebooks/tmp_directory/*
16
+ notebooks/save_folder/*
17
+ notebooks/cache
18
+ notebooks/\[draft*
19
+ notebooks/\[scratch*
20
+ notebooks/labbench/*
21
+ scripts/save_folder/*
22
+ scripts/\[draft*
23
+ scripts/tmp_dir/*
24
+ scripts/tmp_directory/*
25
+ scripts/figures/*
26
+ */scripts/outputs/*
27
+ */scripts/wandb/*
28
+ */results/*
29
+ bioagentos/agent/popper_agent.py
30
+ bioagentos/scripts/biorxiv_results
31
+ bioagentos/tool/data_tool/old_data_tool/*
32
+ examples/*.png
33
+ examples/*.csv
34
+ examples/*.fa
35
+ scratch/
36
+ packages/
37
+ data
38
+ dataset
39
+ logs
40
+ scripts/gradio
41
+ data_lake
42
+ __pycache__/
43
+ *.py[cod]
44
+ *$py.class
45
+ res/
46
+ examples/tmp*
47
+ bioagentos/task/lab_bench.py
48
+ *database/
49
+ .history/
50
+
51
+ temp.*
52
+
53
+ scripts/analysis/all_tools.txt
54
+
55
+ BIOMNI_GRADIO/biomni/tool/files/*
56
+
57
+
58
+ # C extensions
59
+ *.so
60
+
61
+ # Distribution / packaging
62
+ .Python
63
+ build/
64
+ develop-eggs/
65
+ dist/
66
+ downloads/
67
+ eggs/
68
+ .eggs/
69
+ lib/
70
+ lib64/
71
+ parts/
72
+ sdist/
73
+ var/
74
+ wheels/
75
+ share/python-wheels/
76
+ *.egg-info/
77
+ .installed.cfg
78
+ *.egg
79
+ MANIFEST
80
+
81
+ # PyInstaller
82
+ # Usually these files are written by a python script from a template
83
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
84
+ *.manifest
85
+ *.spec
86
+
87
+ # Installer logs
88
+ pip-log.txt
89
+ pip-delete-this-directory.txt
90
+
91
+ # Unit test / coverage reports
92
+ htmlcov/
93
+ .tox/
94
+ .nox/
95
+ .coverage
96
+ .coverage.*
97
+ .cache
98
+ nosetests.xml
99
+ coverage.xml
100
+ *.cover
101
+ *.py,cover
102
+ .hypothesis/
103
+ .pytest_cache/
104
+ cover/
105
+ test.ipynb
106
+
107
+ # Translations
108
+ *.mo
109
+ *.pot
110
+
111
+ # Django stuff:
112
+ *.log
113
+ local_settings.py
114
+ db.sqlite3
115
+ db.sqlite3-journal
116
+
117
+ # Flask stuff:
118
+ instance/
119
+ .webassets-cache
120
+
121
+ # Scrapy stuff:
122
+ .scrapy
123
+
124
+ # Sphinx documentation
125
+ docs/_build/
126
+
127
+ # PyBuilder
128
+ .pybuilder/
129
+ target/
130
+
131
+ # Jupyter Notebook
132
+ .ipynb_checkpoints
133
+
134
+ # IPython
135
+ profile_default/
136
+ ipython_config.py
137
+
138
+ # pyenv
139
+ # For a library or package, you might want to ignore these files since the code is
140
+ # intended to run in multiple environments; otherwise, check them in:
141
+ # .python-version
142
+
143
+ # pipenv
144
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
145
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
146
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
147
+ # install all needed dependencies.
148
+ #Pipfile.lock
149
+
150
+ # poetry
151
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
152
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
153
+ # commonly ignored for libraries.
154
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
155
+ #poetry.lock
156
+
157
+ # pdm
158
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
159
+ #pdm.lock
160
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
161
+ # in version control.
162
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
163
+ .pdm.toml
164
+ .pdm-python
165
+ .pdm-build/
166
+
167
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
168
+ __pypackages__/
169
+
170
+ # Celery stuff
171
+ celerybeat-schedule
172
+ celerybeat.pid
173
+
174
+ # SageMath parsed files
175
+ *.sage.py
176
+
177
+ # Environments
178
+ .env
179
+ .venv
180
+ env/
181
+ venv/
182
+ ENV/
183
+ env.bak/
184
+ venv.bak/
185
+ .envrc
186
+ uv.lock
187
+
188
+ # Spyder project settings
189
+ .spyderproject
190
+ .spyproject
191
+
192
+ # Rope project settings
193
+ .ropeproject
194
+
195
+ # mkdocs documentation
196
+ /site
197
+
198
+ # mypy
199
+ .mypy_cache/
200
+ .dmypy.json
201
+ dmypy.json
202
+
203
+ *.jsonl
204
+
205
+ # Pyre type checker
206
+ .pyre/
207
+
208
+ # pytype static type analyzer
209
+ .pytype/
210
+
211
+ # Cython debug symbols
212
+ cython_debug/
213
+
214
+ # macOS system files
215
+ .DS_Store
216
+ **/.DS_Store
217
+
218
+ # DDInter data files
219
+ biomni/tool/schema_db/ddinter_*.pkl
220
+ data/ddinter_raw/
221
+
222
+ # PyCharm
223
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
224
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
225
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
226
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
227
+ #.idea/
228
+
229
+ # VSCode
230
+ /.vscode
231
+
232
+ # Sphinx build
233
+ /docs/build/
234
+ /docs/source/api/
Biomni/.pre-commit-config.yaml ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fail_fast: false
2
+ default_language_version:
3
+ python: python3
4
+ default_stages:
5
+ - pre-commit
6
+ - pre-push
7
+ minimum_pre_commit_version: 2.16.0
8
+ repos:
9
+ - repo: https://github.com/biomejs/pre-commit
10
+ rev: v2.3.10
11
+ hooks:
12
+ - id: biome-format
13
+ - repo: https://github.com/astral-sh/ruff-pre-commit
14
+ rev: v0.14.11
15
+ hooks:
16
+ - id: ruff-check
17
+ args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes]
18
+ - id: ruff-format
19
+ - repo: https://github.com/pre-commit/pre-commit-hooks
20
+ rev: v6.0.0
21
+ hooks:
22
+ - id: detect-private-key
23
+ - id: check-ast
24
+ - id: end-of-file-fixer
25
+ - id: mixed-line-ending
26
+ args: [--fix=lf]
27
+ - id: trailing-whitespace
28
+ - id: check-case-conflict
29
+ - id: check-added-large-files
30
+ - id: check-toml
31
+ - id: check-yaml
32
+ - id: check-merge-conflict
33
+ - id: no-commit-to-branch
34
+ args: ["--branch=main"]
35
+ #- repo: https://github.com/pre-commit/mirrors-mypy
36
+ # rev: v1.16.1
37
+ # hooks:
38
+ # - id: mypy
39
+ # args: [--no-strict-optional, --ignore-missing-imports]
40
+ # additional_dependencies:
41
+ # ["types-setuptools", "types-requests", "types-attrs"]
Biomni/DETAILS.md ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DETAILS.md
2
+
3
+ 🔍 **Powered by [Detailer](https://detailer.ginylil.com)** - Context-aware codebase analysis
4
+
5
+
6
+
7
+ ---
8
+
9
+ ## 1. Project Overview
10
+
11
+ ### Project Purpose & Domain
12
+
13
+ This project is a comprehensive **biomedical AI toolkit and research platform** designed to facilitate **biomedical data analysis, knowledge extraction, and AI-driven reasoning**. It integrates large language models (LLMs), domain-specific bioinformatics tools, and scientific data processing pipelines to enable:
14
+
15
+ - Automated extraction of biomedical knowledge from literature (e.g., bioRxiv papers)
16
+ - Querying and integration of diverse biomedical databases and APIs
17
+ - Execution of domain-specific computational biology and physiology analyses
18
+ - AI agent orchestration for complex biomedical reasoning and tool invocation
19
+ - Benchmarking and evaluation of biomedical tasks and datasets
20
+
21
+ ### Target Users and Use Cases
22
+
23
+ - **Biomedical researchers and data scientists** seeking to automate literature mining, data retrieval, and analysis workflows.
24
+ - **Bioinformaticians** requiring integrated access to multiple biological databases and computational tools.
25
+ - **AI researchers** interested in applying LLMs and autonomous agents to biomedical problem solving.
26
+ - **Developers and integrators** building domain-specific AI pipelines and scientific workflows.
27
+ - Use cases include:
28
+ - Extracting structured biomedical tasks and entities from scientific papers
29
+ - Querying gene, protein, disease, and pathway databases via natural language prompts
30
+ - Running computational models of biological systems (e.g., metabolic networks, signaling)
31
+ - Performing image analysis and quantitative pathology workflows
32
+ - Orchestrating multi-step AI reasoning with tool use and self-criticism
33
+
34
+ ### Core Business Logic and Domain Models
35
+
36
+ - **Biomedical domain models**: gene IDs, protein structures, pathways, disease ontologies, experimental assays.
37
+ - **Task abstractions**: benchmark tasks with prompt/response evaluation (e.g., humanity last exam, lab bench).
38
+ - **Tool metadata schemas**: declarative descriptions of biomedical tools and APIs for dynamic invocation.
39
+ - **AI agent workflows**: ReAct-style reasoning graphs integrating LLMs, tool calls, retrieval, and self-critique.
40
+ - **Data models**: structured JSON, pandas DataFrames, numpy arrays representing biological data and analysis results.
41
+
42
+ ---
43
+
44
+ ## 2. Architecture and Structure
45
+
46
+ ### High-Level Architecture
47
+
48
+ The system is organized into modular layers and components:
49
+
50
+ - **Core Library (`biomni/`)**: Contains main application logic, including:
51
+ - **Agent framework (`biomni/agent/`)**: Implements autonomous AI agents using LLMs and workflow graphs.
52
+ - **Task definitions (`biomni/task/`)**: Abstract base and concrete biomedical benchmark tasks.
53
+ - **Tool implementations (`biomni/tool/`)**: Domain-specific analysis functions, API clients, and computational biology workflows.
54
+ - **Tool metadata (`biomni/tool/tool_description/`)**: Declarative schemas describing tool APIs and parameters.
55
+ - **Model components (`biomni/model/`)**: AI-driven resource retriever for selecting relevant tools and data.
56
+ - **Utility modules (`biomni/utils.py`, `biomni/llm.py`, `biomni/env_desc.py`)**: Helpers for LLM instantiation, system commands, environment descriptions.
57
+ - **Versioning (`biomni/version.py`)**: Package version management.
58
+
59
+ - **Environment Setup (`biomni_env/`)**: Scripts and configuration files for reproducible environment provisioning, including:
60
+ - Conda environment YAMLs (`environment.yml`, `bio_env.yml`)
61
+ - R package specifications (`r_packages.yml`)
62
+ - CLI tools installer (`install_cli_tools.sh`)
63
+ - Shell scripts for environment setup (`setup.sh`, `setup_path.sh`)
64
+
65
+ - **Scripts (`biomni/biorxiv_scripts/`)**: Data processing pipelines for literature mining and task extraction.
66
+
67
+ - **Documentation and Configuration**:
68
+ - Root-level files: `README.md`, `CONTRIBUTION.md`, `pyproject.toml`, `.pre-commit-config.yaml`.
69
+
70
+ ---
71
+
72
+ ### Complete Repository Structure
73
+
74
+ ```
75
+ .
76
+ ├── biomni/ (90 items)
77
+ │ ├── agent/
78
+ │ │ ├── __init__.py
79
+ │ │ ├── a1.py
80
+ │ │ ├── env_collection.py
81
+ │ │ ├── qa_llm.py
82
+ │ │ └── react.py
83
+ │ ├── biorxiv_scripts/
84
+ │ │ ├── extract_biorxiv_tasks.py
85
+ │ │ ├── generate_function.py
86
+ │ │ └── process_all_subjects.py
87
+ │ ├── model/
88
+ │ │ ├── __init__.py
89
+ │ │ └── retriever.py
90
+ │ ├── task/
91
+ │ │ ├── __init__.py
92
+ │ │ ├── base_task.py
93
+ │ │ ├── hle.py
94
+ │ │ └── lab_bench.py
95
+ │ ├── tool/ (65 items)
96
+ │ │ ├── schema_db/ (25 items)
97
+ │ │ │ ├── cbioportal.pkl
98
+ │ │ │ ├── clinvar.pkl
99
+ │ │ │ ├── dbsnp.pkl
100
+ │ │ │ ├── emdb.pkl
101
+ │ │ │ ├── ensembl.pkl
102
+ │ │ │ ├── geo.pkl
103
+ │ │ │ ├── gnomad.pkl
104
+ │ │ │ ├── gtopdb.pkl
105
+ │ │ │ ├── gwas_catalog.pkl
106
+ │ │ │ ├── interpro.pkl
107
+ │ │ │ └── ... (15 more files)
108
+ │ │ ├── tool_description/ (18 items)
109
+ │ │ │ ├── biochemistry.py
110
+ │ │ │ ├── bioengineering.py
111
+ │ │ │ ├── biophysics.py
112
+ │ │ │ ├── cancer_biology.py
113
+ │ │ │ ├── cell_biology.py
114
+ │ │ │ ├── database.py
115
+ │ │ │ ├── genetics.py
116
+ │ │ │ ├── genomics.py
117
+ │ │ │ ├── immunology.py
118
+ │ │ │ ├── literature.py
119
+ │ │ │ ├── microbiology.py
120
+ │ │ │ ├── molecular_biology.py
121
+ │ │ │ ├── pathology.py
122
+ │ │ │ ├── pharmacology.py
123
+ │ │ │ ├── physiology.py
124
+ │ │ │ ├── support_tools.py
125
+ │ │ │ ├── synthetic_biology.py
126
+ │ │ │ └── systems_biology.py
127
+ │ │ ├── __init__.py
128
+ │ │ ├── biochemistry.py
129
+ │ │ ├── bioengineering.py
130
+ │ │ ├── biophysics.py
131
+ │ │ ├── cancer_biology.py
132
+ │ │ ├── cell_biology.py
133
+ │ │ ├── database.py
134
+ │ │ ├── genetics.py
135
+ │ │ └── ... (12 more files)
136
+ │ ├── __init__.py
137
+ │ ├── env_desc.py
138
+ │ ├── llm.py
139
+ │ ├── utils.py
140
+ │ └── version.py
141
+ ├── biomni_env/ (9 items)
142
+ │ ├── README.md
143
+ │ ├── bio_env.yml
144
+ │ ├── cli_tools_config.json
145
+ │ ├── environment.yml
146
+ │ ├── install_cli_tools.sh
147
+ │ ├── install_r_packages.R
148
+ │ ├── r_packages.yml
149
+ │ ├── setup.sh
150
+ │ └── setup_path.sh
151
+ ├── figs/
152
+ │ └── biomni_logo.png
153
+ ├── tutorials/
154
+ │ ├── examples/
155
+ │ │ └── cloning.ipynb
156
+ │ ├── 101_biomni.ipynb
157
+ │ └── biomni_101.ipynb
158
+ ├── .gitignore
159
+ ├── .pre-commit-config.yaml
160
+ ├── CONTRIBUTION.md
161
+ ├── LICENSE
162
+ ├── README.md
163
+ └── pyproject.toml
164
+ ```
165
+
166
+ ---
167
+
168
+ ## 3. Technical Implementation Details
169
+
170
+ ### Core Modules and Their Roles
171
+
172
+ #### `biomni/agent/`
173
+
174
+ - Implements autonomous AI agents using the **ReAct paradigm**:
175
+ - `react.py`: Main ReAct agent class managing reasoning, tool invocation, retrieval, and self-criticism workflows.
176
+ - `env_collection.py`: Environment and data retrieval utilities.
177
+ - `qa_llm.py`: Question-answering LLM wrappers.
178
+ - `a1.py`: Possibly experimental or auxiliary agent code.
179
+
180
+ - Uses **langgraph** for workflow graph orchestration and **langchain** for LLM integration.
181
+
182
+ #### `biomni/task/`
183
+
184
+ - Defines **benchmark tasks** with a common interface:
185
+ - `base_task.py`: Abstract base class specifying methods like `get_example()`, `evaluate()`, `output_class()`.
186
+ - `hle.py`: "Humanity Last Exam" task implementation.
187
+ - `lab_bench.py`: Lab bench dataset task.
188
+
189
+ - Tasks load data (e.g., parquet files), generate prompts, and evaluate LLM responses.
190
+
191
+ #### `biomni/tool/`
192
+
193
+ - Contains **domain-specific scientific analysis functions** organized by subdomains:
194
+ - `biochemistry.py`, `bioengineering.py`, `biophysics.py`, `cancer_biology.py`, `cell_biology.py`, `genetics.py`, `pathology.py`, `physiology.py`, `systems_biology.py`, etc.
195
+ - Each file implements multiple functions performing analyses, simulations, or data processing workflows.
196
+ - Functions accept input files/parameters and return detailed textual logs and output files.
197
+
198
+ - **API client modules** (e.g., `database.py`) provide facade functions to query external biomedical databases (UniProt, GWAS Catalog, Ensembl, etc.) via REST or GraphQL APIs, often using LLMs to generate query payloads from natural language prompts.
199
+
200
+ - **Tool registry (`tool_registry.py`)** manages metadata about available tools, supporting dynamic registration and lookup.
201
+
202
+ #### `biomni/tool/tool_description/`
203
+
204
+ - Contains **declarative metadata schemas** describing tool APIs:
205
+ - Each file exports a `description` list of dictionaries defining tool names, descriptions, required and optional parameters with types and defaults.
206
+ - Supports **dynamic API generation, validation, and documentation**.
207
+ - Organized by biological domain (e.g., genetics, immunology, pathology).
208
+
209
+ #### `biomni/model/retriever.py`
210
+
211
+ - Implements `ToolRetriever` class for **AI-driven resource selection**:
212
+ - Uses LLMs (OpenAI or Anthropic) to parse user queries and select relevant tools, datasets, and libraries.
213
+ - Encapsulates prompt formatting and response parsing logic.
214
+
215
+ #### `biomni/utils.py` and `biomni/llm.py`
216
+
217
+ - `utils.py`: Utility functions for running system commands (R, Bash), file operations, schema generation, logging, and colorized printing.
218
+ - `llm.py`: Factory functions to instantiate LLMs (OpenAI, Anthropic) with configurable parameters.
219
+
220
+ #### `biomni/env_desc.py`
221
+
222
+ - Contains **environment and dataset descriptions**, acting as a centralized metadata repository for datasets and experimental environments.
223
+
224
+ ---
225
+
226
+ ### Environment Setup (`biomni_env/`)
227
+
228
+ - `setup.sh`: Main shell script to create conda environment, install R packages, and CLI bioinformatics tools.
229
+ - `install_cli_tools.sh`: Automates downloading, compiling, and installing external bioinformatics command-line tools, managing PATH and verification.
230
+ - `r_packages.yml`: Lists R packages required.
231
+ - `environment.yml` and `bio_env.yml`: Conda environment specifications.
232
+ - `setup_path.sh`: Shell script to update environment variables for CLI tools.
233
+
234
+ ---
235
+
236
+ ### Entry Points and Execution Flow
237
+
238
+ - **Agent usage**: Instantiate `react` agent from `biomni.agent.react`, configure with tools and retrieval, then call `go(prompt)` to run reasoning workflows.
239
+ - **Task evaluation**: Use classes in `biomni.task` to load datasets, generate prompts, and evaluate LLM outputs.
240
+ - **Tool invocation**: Call functions in `biomni.tool` modules or use API facades in `database.py` to query external resources.
241
+ - **Metadata-driven tool discovery**: Use `tool_registry.py` and `tool_description` schemas to dynamically discover and validate tools.
242
+ - **Environment setup**: Run `biomni_env/setup.sh` to provision environment and install dependencies.
243
+
244
+ ---
245
+
246
+ ## 4. Development Patterns and Standards
247
+
248
+ ### Code Organization Principles
249
+
250
+ - **Modular design**: Clear separation of concerns by domain and functionality (agent, task, tool, model).
251
+ - **Functional programming style**: Most analysis modules use standalone functions with explicit inputs and outputs.
252
+ - **Declarative metadata**: Tool descriptions and schemas are separated from implementation, enabling dynamic validation and UI generation.
253
+ - **Abstract base classes**: Used in `biomni.task.base_task` to enforce consistent task interfaces.
254
+ - **Factory pattern**: Used in `llm.py` to instantiate LLMs based on configuration.
255
+ - **Strategy pattern**: Task implementations and tool retrieval use interchangeable strategies.
256
+
257
+ ### Testing and Coverage
258
+
259
+ - No explicit test files detected; testing likely manual or via notebooks (`tutorials/`).
260
+ - Tasks and tools return detailed logs suitable for manual verification.
261
+ - Metadata schemas facilitate automated validation of inputs.
262
+
263
+ ### Error Handling and Logging
264
+
265
+ - Use of try-except blocks around external calls and subprocesses.
266
+ - Logging via custom callback handlers (`PromptLogger`, `NodeLogger`) in LLM interactions.
267
+ - Utilities provide colorized printing and error wrappers for robustness.
268
+
269
+ ### Configuration Management
270
+
271
+ - Environment variables for API keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`).
272
+ - YAML and JSON files for environment and tool configuration.
273
+ - Dynamic loading of schemas from pickle files for API request generation.
274
+ - CLI tools and R packages installed via scripted environment setup.
275
+
276
+ ---
277
+
278
+ ## 5. Integration and Dependencies
279
+
280
+ ### External Libraries
281
+
282
+ - **LLM & AI Frameworks**: `langchain_core`, `langchain_openai`, `langchain_anthropic`
283
+ - **Scientific Computing**: `numpy`, `pandas`, `scipy`, `scikit-image`, `matplotlib`, `BioPython`, `cobra`, `sklearn`
284
+ - **Data Processing**: `pickle`, `json`, `requests`, `PyPDF2`
285
+ - **System and OS**: `subprocess`, `os`, `sys`, `tempfile`, `multiprocessing`
286
+ - **Others**: `tqdm` (progress bars), `enum`, `ast` (code introspection)
287
+
288
+ ### External APIs and Data Sources
289
+
290
+ - Biomedical databases: UniProt, GWAS Catalog, Ensembl, ClinVar, dbSNP, EMDB, GEO, GnomAD, InterPro, etc.
291
+ - Bioinformatics tools: PLINK, IQ-TREE, GCTA, MACS2, samtools, LUMPY, installed via CLI tools installer.
292
+ - R packages for statistical and bioinformatics analyses.
293
+
294
+ ### Build and Deployment Dependencies
295
+
296
+ - Python 3 environment managed via Conda (`environment.yml`).
297
+ - R environment with specified packages (`r_packages.yml`).
298
+ - Shell scripts for CLI tool installation and environment setup.
299
+ - Pre-commit hooks for code quality and security.
300
+
301
+ ---
302
+
303
+ ## 6. Usage and Operational Guidance
304
+
305
+ ### Getting Started
306
+
307
+ 1. **Environment Setup**
308
+ - Run `biomni_env/setup.sh` to create the Conda environment, install R packages, and CLI tools.
309
+ - Source `biomni_env/setup_path.sh` or add it to your shell profile to configure PATH.
310
+
311
+ 2. **API Keys**
312
+ - Set environment variables `OPENAI_API_KEY` and/or `ANTHROPIC_API_KEY` for LLM access.
313
+
314
+ 3. **Running Agents**
315
+ - Import and instantiate the `react` agent from `biomni.agent.react`.
316
+ - Configure with desired tools and retrieval options.
317
+ - Call `go(prompt)` to execute reasoning workflows.
318
+
319
+ 4. **Executing Tasks**
320
+ - Use classes in `biomni.task` to load datasets and evaluate LLM responses.
321
+ - Implement new tasks by subclassing `base_task` and following the interface.
322
+
323
+ 5. **Querying Databases**
324
+ - Use `biomni.tool.database` functions (e.g., `query_uniprot`, `query_gwas_catalog`) to retrieve data via natural language or direct parameters.
325
+
326
+ 6. **Extending Tools**
327
+ - Add new tool metadata in `biomni/tool/tool_description/` as structured dictionaries.
328
+ - Implement corresponding analysis functions in `biomni/tool/`.
329
+ - Register tools in `tool_registry.py` for discovery.
330
+
331
+ ### Monitoring and Debugging
332
+
333
+ - Use logging callbacks (`PromptLogger`, `NodeLogger`) to trace LLM interactions.
334
+ - Check output logs returned by analysis functions for detailed execution info.
335
+ - Use pre-commit hooks to maintain code quality.
336
+
337
+ ### Performance and Scalability
338
+
339
+ - Modular design allows parallel execution of tasks and tools.
340
+ - Timeout wrappers in agent tools prevent hanging executions.
341
+ - Use of efficient numerical libraries (`numpy`, `scipy`) for computational tasks.
342
+ - Large data handled via streaming and chunking (e.g., PDF text extraction).
343
+
344
+ ### Security Considerations
345
+
346
+ - API keys managed via environment variables, not hardcoded.
347
+ - Pre-commit hooks include security checks.
348
+ - External tool installations verified via version commands.
349
+
350
+ ### Observability
351
+
352
+ - Progress bars (`tqdm`) used in data processing scripts.
353
+ - Structured logs and JSON outputs facilitate downstream analysis.
354
+ - Agent workflows produce detailed message histories for audit.
355
+
356
+ ---
357
+
358
+ ## Summary
359
+
360
+ This project is a **modular, extensible biomedical AI platform** integrating **LLM-powered agents**, **domain-specific scientific tools**, and **metadata-driven APIs** to automate complex biomedical research workflows. It emphasizes **declarative tool descriptions**, **dynamic resource retrieval**, and **robust environment provisioning** to enable researchers and developers to build, evaluate, and extend AI-driven biomedical applications efficiently.
361
+
362
+ ---
363
+
364
+ # End of DETAILS.md
Biomni/LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
Biomni/MANIFEST.in ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MANIFEST.in
2
+
3
+ # Include all python files from the biomni package
4
+ recursive-include biomni *.py
5
+
6
+ # Include the .pkl database files
7
+ recursive-include biomni/tool/schema_db *.pkl
8
+
9
+ # Include specific files from biomni_env, but not the biomni_tools subdirectory
10
+ recursive-include biomni_env *.py *.sh *.yml *.yaml *.txt *.md *.json *.R
11
+
12
+ # Exclude specific directories that are not part of the package
13
+ prune tutorials/data
14
+ prune biomni_env/biomni_tools
15
+
16
+ # Exclude build artifacts and caches
17
+ global-exclude *.py[co]
18
+ global-exclude __pycache__
19
+ global-exclude .ruff_cache
20
+ prune build
21
+ prune dist
22
+ prune *.egg-info
23
+
24
+ # Include other necessary files
25
+ include README.md
26
+ include LICENSE
27
+ include pyproject.toml
Biomni/Q13398_ChIP-seq_Meta-clusters.bb ADDED
Binary file (31.5 kB). View file
 
Biomni/biomni/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .version import __version__
2
+
3
+ __all__ = ["__version__"]
Biomni/biomni/agent/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from biomni.agent.a1 import A1 # noqa: F401
Biomni/biomni/agent/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (215 Bytes). View file
 
Biomni/biomni/agent/a1.py ADDED
The diff for this file is too large to render. See raw diff
 
Biomni/biomni/agent/env_collection.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ from typing import Any
5
+
6
+ from langchain_core.prompts import ChatPromptTemplate
7
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
8
+
9
+ from biomni.agent.base_agent import base_agent
10
+
11
+
12
+ class PaperTaskExtractor(base_agent):
13
+ """Agent that extracts generalizable tasks/experiments from academic papers.
14
+ It processes papers in chunks and identifies common research tasks that could be shared across papers.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ llm="claude-3-7-sonnet-20250219",
20
+ cheap_llm=None,
21
+ tools=None,
22
+ chunk_size=4000,
23
+ chunk_overlap=400,
24
+ ):
25
+ """Initialize the PaperTaskExtractor agent.
26
+
27
+ Args:
28
+ llm (str): The LLM model to use
29
+ cheap_llm (str, optional): A cheaper LLM for simpler tasks
30
+ tools (list, optional): Any tools to use (not needed for this agent)
31
+ chunk_size (int): Size of text chunks for processing
32
+ chunk_overlap (int): Overlap between chunks
33
+
34
+ """
35
+ super().__init__(llm, cheap_llm, tools)
36
+ self.chunk_size = chunk_size
37
+ self.chunk_overlap = chunk_overlap
38
+ self.log = []
39
+ self.configure()
40
+
41
+ def configure(self):
42
+ """Configure the agent with appropriate prompts."""
43
+ # Prompt for analyzing paper chunks
44
+ self.chunk_analysis_prompt = """You are a research methodology expert specializing in identifying computational tasks and data analysis procedures in academic papers.
45
+
46
+ Your job is to analyze chunks of academic papers and identify ONLY the most common, generalizable computational tasks that are widely used across biomedical research and can be implemented with Python or Linux code.
47
+
48
+ STRICT GUIDELINES:
49
+ 1. ONLY extract tasks that are extremely common and standard in computational biomedical research
50
+ 2. Each task MUST have clear, well-defined inputs and outputs
51
+ 3. Tasks MUST be generalizable across many different papers and research questions
52
+ 4. Be VERY selective - only include tasks that appear in hundreds of papers
53
+ 5. If a task is specific to this paper, unclear, or not widely used, DO NOT include it
54
+ 6. Focus on computational tasks that can be implemented with Python or Linux code
55
+ 7. Each task should be something that could be implemented as a function with clear inputs/outputs
56
+ 8. Also identify commonly used databases and software packages mentioned in the text
57
+ 9. Tasks MUST be CONCRETE and SPECIFIC - include exact methodological details
58
+ 10. Avoid vague task names like "Statistical Analysis" - instead use specific protocol names like "Two-way ANOVA with Tukey's Post-hoc Test using SciPy"
59
+ 11. DO NOT include wet lab procedures that cannot be implemented computationally
60
+ 12. ONLY include tasks that could be automated with code
61
+
62
+ For the following chunk of text from an academic paper, provide:
63
+ 1. A list of ONLY the most common, generalizable COMPUTATIONAL tasks identified (be extremely selective)
64
+ 2. For each task, clearly define:
65
+ - Task name: A SPECIFIC and CONCRETE name with methodological details (e.g., "RNA-seq Differential Expression Analysis with DESeq2" instead of just "Gene Expression Analysis")
66
+ - Input: What SPECIFIC data or parameters the task requires
67
+ - Output: What SPECIFIC data or results the task produces
68
+ - Code implementation: How this task could be implemented with Python or Linux code, including key libraries/packages
69
+ - Frequency: How common this computational task is in biomedical research
70
+ - Standard methods: The established computational techniques used to perform this task
71
+ - Example: A brief description of how THIS specific paper uses this task (with specific details from the paper)
72
+ 3. A list of commonly used databases mentioned in the text (if any)
73
+ 4. A list of commonly used software packages/tools mentioned in the text (if any)
74
+
75
+ PAPER CHUNK:
76
+ {chunk_text}
77
+
78
+ Remember, it's better to return NO tasks than to include tasks that aren't extremely common, generalizable, and implementable with code. Quality over quantity is essential. Tasks MUST be CONCRETE with SPECIFIC methodological details and MUST be implementable with Python or Linux code.
79
+ """
80
+
81
+ # Prompt for consolidating tasks
82
+ self.consolidation_prompt = """You are a research methodology expert. Your task is to consolidate lists of computational research tasks extracted from different chunks of an academic paper.
83
+
84
+ BE EXTREMELY SELECTIVE. Only include tasks that are:
85
+ 1. Fundamental to computational biomedical research
86
+ 2. Used in hundreds of papers across different subfields
87
+ 3. Have clear, well-defined inputs and outputs
88
+ 4. Represent standard computational approaches
89
+ 5. Could be implemented as a function with specific inputs and outputs
90
+ 6. Are CONCRETE and SPECIFIC with exact methodological details
91
+ 7. Can be implemented with Python or Linux code
92
+ 8. Are computational in nature, not wet lab procedures
93
+
94
+ REMOVE any tasks that:
95
+ - Are specific to a particular paper or dataset
96
+ - Lack clear inputs or outputs
97
+ - Are not widely used across biomedical research
98
+ - Are vague or poorly defined
99
+ - Represent niche or specialized techniques
100
+ - Have generic names without specific methodological details
101
+ - Cannot be implemented with code
102
+ - Require physical lab equipment or manual intervention
103
+
104
+ The output should be a JSON object with three main keys:
105
+ 1. "tasks": A list of task objects, where each task has:
106
+ - "task_name": A SPECIFIC and CONCRETE name with methodological details (e.g., "Single-cell RNA-seq Clustering with Seurat" instead of just "Cell Clustering")
107
+ - "description": A clear description of what the computational task does
108
+ - "inputs": SPECIFIC data types or parameters the task requires
109
+ - "outputs": SPECIFIC data types or results the task produces
110
+ - "code_implementation": How this task could be implemented with Python or Linux code, including key libraries/packages and a brief pseudocode example
111
+ - "frequency": How common this computational task is in biomedical research
112
+ - "standard_methods": The established computational techniques used to perform this task
113
+ - "example": A specific example from THIS paper showing how the task was used (with concrete details)
114
+
115
+ 2. "databases": A list of database objects, where each database has:
116
+ - "name": The name of the database
117
+ - "description": A brief description of what the database contains
118
+ - "url": The URL of the database (if mentioned)
119
+ - "usage": How the database is commonly used in computational biomedical research
120
+ - "example": How this specific paper uses the database
121
+
122
+ 3. "software": A list of software package objects, where each package has:
123
+ - "name": The name of the software package
124
+ - "description": A brief description of what the software does
125
+ - "url": The URL or reference to the software (if mentioned)
126
+ - "usage": How the software is commonly used in computational biomedical research
127
+ - "example": How this specific paper uses the software
128
+
129
+ EXTRACTED INFORMATION FROM PAPER CHUNKS:
130
+ {task_lists}
131
+
132
+ Be ruthless in filtering - it's better to return a few truly common computational tasks than many that aren't universal or implementable with code.
133
+ Respond with only a valid JSON object containing the three lists described above.
134
+ """
135
+
136
+ def process_paper(self, paper_text: str) -> dict[str, list[dict[str, Any]]]:
137
+ """Process a paper and extract generalizable tasks/experiments, databases, and software.
138
+
139
+ Args:
140
+ paper_text (str): The full text of the paper
141
+
142
+ Returns:
143
+ Dict[str, List[Dict[str, Any]]]: A dictionary with tasks, databases, and software
144
+
145
+ """
146
+ # Split the paper into chunks
147
+ text_splitter = RecursiveCharacterTextSplitter(
148
+ chunk_size=self.chunk_size,
149
+ chunk_overlap=self.chunk_overlap,
150
+ length_function=len,
151
+ separators=["\n\n", "\n", ". ", " ", ""],
152
+ )
153
+ chunks = text_splitter.split_text(paper_text)
154
+
155
+ # Process each chunk to extract tasks
156
+ chunk_results = []
157
+ for i, chunk in enumerate(chunks):
158
+ print(f"Processing chunk {i + 1}/{len(chunks)}...")
159
+ chunk_tasks = self._process_chunk(chunk)
160
+ chunk_results.append(chunk_tasks)
161
+
162
+ # Consolidate tasks from all chunks
163
+ consolidated_results = self._consolidate_tasks(chunk_results)
164
+ return consolidated_results
165
+
166
+ def _process_chunk(self, chunk_text: str) -> str:
167
+ """Process a single chunk of the paper to extract tasks.
168
+
169
+ Args:
170
+ chunk_text (str): The chunk text to process
171
+
172
+ Returns:
173
+ str: Extracted tasks from this chunk
174
+
175
+ """
176
+ prompt = self.chunk_analysis_prompt.format(chunk_text=chunk_text)
177
+ message = self.llm.invoke(prompt)
178
+ return message.content
179
+
180
+ def _consolidate_tasks(self, chunk_results: list[str]) -> dict[str, list[dict[str, Any]]]:
181
+ """Consolidate tasks, databases, and software extracted from different chunks into a unified structure.
182
+
183
+ Args:
184
+ chunk_results (List[str]): Results from each chunk
185
+
186
+ Returns:
187
+ Dict[str, List[Dict[str, Any]]]: Consolidated information with tasks, databases, and software
188
+
189
+ """
190
+ # Combine all chunk results
191
+ all_tasks = "\n\n===== CHUNK SEPARATOR =====\n\n".join(chunk_results)
192
+
193
+ # Use the consolidation prompt to merge and organize tasks
194
+ prompt = self.consolidation_prompt.format(task_lists=all_tasks)
195
+ response = self.llm.invoke(prompt)
196
+
197
+ # Extract the JSON from the response
198
+ try:
199
+ # Try to parse the entire response as JSON
200
+ result = json.loads(response.content)
201
+ except json.JSONDecodeError:
202
+ # If that fails, try to extract JSON from the text
203
+ try:
204
+ # Look for JSON-like content between triple backticks
205
+ json_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", response.content)
206
+ if json_match:
207
+ result = json.loads(json_match.group(1))
208
+ else:
209
+ # Fallback: just return the text response
210
+ return {
211
+ "tasks": [
212
+ {
213
+ "error": "Could not parse JSON",
214
+ "raw_response": response.content,
215
+ }
216
+ ],
217
+ "databases": [],
218
+ "software": [],
219
+ }
220
+ except Exception:
221
+ return {
222
+ "tasks": [
223
+ {
224
+ "error": "Could not parse JSON",
225
+ "raw_response": response.content,
226
+ }
227
+ ],
228
+ "databases": [],
229
+ "software": [],
230
+ }
231
+
232
+ # Ensure the result has the expected structure
233
+ if not isinstance(result, dict):
234
+ result = {
235
+ "tasks": result if isinstance(result, list) else [],
236
+ "databases": [],
237
+ "software": [],
238
+ }
239
+
240
+ # Ensure all required keys exist
241
+ for key in ["tasks", "databases", "software"]:
242
+ if key not in result:
243
+ result[key] = []
244
+
245
+ return result
246
+
247
+ def go(self, paper_text: str):
248
+ """Process a paper and return the extracted tasks, databases, and software.
249
+
250
+ Args:
251
+ paper_text (str): The full text of the paper
252
+
253
+ Returns:
254
+ tuple: (log, results) where log is a list of processing steps and results is the final result
255
+
256
+ """
257
+ self.log = []
258
+ self.log.append(
259
+ (
260
+ "user",
261
+ "Extract only the most common and generalizable biomedical research tasks, databases, and software from this paper",
262
+ )
263
+ )
264
+
265
+ results = self.process_paper(paper_text)
266
+
267
+ result_str = json.dumps(results, indent=2)
268
+ self.log.append(("assistant", result_str))
269
+
270
+ return self.log, results
271
+
272
+ def save_results(self, results: dict[str, list[dict[str, Any]]], output_path: str):
273
+ """Save the extracted tasks, databases, and software to a JSON file.
274
+
275
+ Args:
276
+ results (Dict[str, List[Dict[str, Any]]]): The extracted tasks, databases, and software
277
+ output_path (str): Path to save the results
278
+
279
+ """
280
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
281
+ with open(output_path, "w") as f:
282
+ json.dump(results, f, indent=2)
283
+ print(f"Results saved to {output_path}")
284
+
285
+ def result_formatting(self, output_class, task_intention):
286
+ """Format the results according to a specific output class.
287
+
288
+ Args:
289
+ output_class: The class to format the output as
290
+ task_intention: Description of the task
291
+
292
+ Returns:
293
+ The formatted result
294
+
295
+ """
296
+ format_check_prompt = ChatPromptTemplate.from_messages(
297
+ [
298
+ (
299
+ "system",
300
+ (
301
+ "You are evaluateGPT, tasked with extract and parse the task output based on the history of an agent. "
302
+ "Review the entire history of messages provided. "
303
+ "Here is the task output requirement: \n"
304
+ f"'{task_intention.replace('{', '{{').replace('}', '}}')}'.\n"
305
+ ),
306
+ ),
307
+ ("placeholder", "{messages}"),
308
+ ]
309
+ )
310
+
311
+ checker_llm = format_check_prompt | self.llm.with_structured_output(output_class)
312
+ result = checker_llm.invoke({"messages": [("user", str(self.log))]}).dict()
313
+ return result
Biomni/biomni/agent/function_generator.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ from biomni.llm import get_llm
4
+
5
+
6
+ class base_agent:
7
+ def __init__(self, llm="claude-3-haiku-20240307", cheap_llm=None, tools=None, temperature=0.7):
8
+ self.tools = tools
9
+ self.llm = get_llm(llm, temperature)
10
+ if cheap_llm is None:
11
+ self.cheap_llm = llm
12
+ else:
13
+ self.cheap_llm = cheap_llm
14
+
15
+ def configure(self):
16
+ pass
17
+
18
+ def go(self, input):
19
+ pass
20
+
21
+
22
+ class FunctionGenerator(base_agent):
23
+ """Agent that generates executable Python code scripts given a task description."""
24
+
25
+ def __init__(self, llm="claude-3-7-sonnet-20250219", cheap_llm=None, temperature=0.7):
26
+ """Initialize the PaperTaskExtractor agent.
27
+
28
+ Args:
29
+ llm (str): The LLM model to use
30
+ cheap_llm (str, optional): A cheaper LLM for simpler tasks
31
+ """
32
+ super().__init__(llm, cheap_llm, temperature)
33
+ self.log = []
34
+ self.configure()
35
+
36
+ def configure(self):
37
+ """Configure the agent with appropriate prompts."""
38
+ # Prompt for Python code generation
39
+ self.system_prompt = """You are a senior Python engineer. Generate robust, idiomatic Python code that solves the user's task. Requirements:
40
+ 1. Output ONLY Python code, ideally inside a single triple-backtick code block.
41
+ 2. Include minimal inline comments and a small docstring.
42
+ 3. Add a `main()` and an `if __name__ == '__main__':` guard when appropriate.
43
+ 4. Avoid external dependencies unless necessary; if used, show `pip` installs in comments.
44
+ 5. Do not include prose before or after the code.
45
+ 6. When applicable, prioritize the use of codes on public repositories, such as HuggingFace or Github
46
+
47
+ Generate Python codes for the following task:
48
+ {task}
49
+ """
50
+
51
+ def _generate_code(self, task_description: str) -> str:
52
+ """Generate codes given a task description.
53
+ Args:
54
+ task_description (str): task descriptions (possibly generated from previous steps)
55
+
56
+ Returns:
57
+ str: generated code string
58
+
59
+ """
60
+ prompt = self.system_prompt.format(task=task_description)
61
+ message = self.llm.invoke(prompt)
62
+ return message.content
63
+
64
+ def _generate_script_filename(self, task_description: str, max_words: int = 6) -> str:
65
+ """
66
+ Generate a safe, meaningful Python script filename from a task description.
67
+ Poised for update: may ask the agent to suggest meaningful names.
68
+
69
+ Parameters:
70
+ -----------
71
+ task_description (str): task descriptions (possibly generated from previous steps)
72
+
73
+ max_words : int
74
+ Maximum number of words to include in the filename.
75
+
76
+ Returns:
77
+ --------
78
+ str
79
+ A lowercase, hyphen-free, safe filename ending in '.py'.
80
+ """
81
+ # Lowercase and remove non-alphanumeric (allow spaces for splitting)
82
+ cleaned = re.sub(r"[^a-zA-Z0-9\s]", "", task_description.lower())
83
+
84
+ # Tokenize and select top words
85
+ words = cleaned.split()
86
+ selected_words = words[:max_words] if words else ["script"]
87
+
88
+ # Join with underscores
89
+ base_name = "_".join(selected_words)
90
+ return f"{base_name}.py"
91
+
92
+ def go(self, task_description: str):
93
+ """Implement the inherited function to get the tasks done.
94
+
95
+ Args:
96
+ task_description (str): task descriptions (possibly generated from previous steps)
97
+
98
+ Returns:
99
+ tuple: (script_filename, results) where script_filename is a generated name for script file and results is the generated codes
100
+
101
+ """
102
+ self.log = []
103
+ self.log.append(
104
+ (
105
+ "user",
106
+ "Generate Python codes given a task description",
107
+ )
108
+ )
109
+
110
+ script_filename = self._generate_script_filename(task_description)
111
+ results = self._generate_code(task_description)
112
+ return script_filename, self._extract_code_block(results)
113
+
114
+ def _extract_code_block(self, s: str) -> str:
115
+ """
116
+ Extract the first fenced code block (``` or ```python) from s.
117
+ """
118
+ m = re.search(r"```(?:python)?\s*(.+?)\s*```", s, flags=re.DOTALL | re.IGNORECASE)
119
+ return m.group(1).strip() if m else None
Biomni/biomni/agent/qa_llm.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.prompts import ChatPromptTemplate
2
+
3
+ from biomni.llm import get_llm
4
+
5
+
6
+ class qa_llm:
7
+ def __init__(self, path="./data", llm="claude-3-haiku-20240307", lab_bench_reproduce=False):
8
+ self.path = path
9
+ self.llm = get_llm(llm)
10
+
11
+ if lab_bench_reproduce:
12
+ self.prompt_modifier = """
13
+ The following is a multiple choice question about biology.
14
+ Please answer by responding with the letter of the correct answer.
15
+
16
+ Think step by step. \n
17
+ """
18
+ else:
19
+ self.prompt_modifier = ""
20
+ self.log = []
21
+
22
+ def configure(self):
23
+ pass
24
+
25
+ def go(self, input):
26
+ self.log = []
27
+ self.log.append(("user", input))
28
+ message = self.llm.invoke(self.prompt_modifier + input)
29
+ self.log.append(("assistant", message.content))
30
+ return [message.content], message.content
31
+
32
+ def result_formatting(self, output_class, task_intention):
33
+ self.format_check_prompt = ChatPromptTemplate.from_messages(
34
+ [
35
+ (
36
+ "system",
37
+ (
38
+ "You are evaluateGPT, tasked with extract and parse the task output based on the history of an agent. "
39
+ "Review the entire history of messages provided. "
40
+ "Here is the task output requirement: \n"
41
+ f"'{task_intention.replace('{', '{{').replace('}', '}}')}'.\n"
42
+ ),
43
+ ),
44
+ ("placeholder", "{messages}"),
45
+ ]
46
+ )
47
+
48
+ checker_llm = self.format_check_prompt | self.llm.with_structured_output(output_class)
49
+ result = checker_llm.invoke({"messages": [("user", str(self.log))]}).dict()
50
+ return result
Biomni/biomni/agent/react.py ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import glob
2
+ import inspect
3
+ import json
4
+ import os
5
+ import signal
6
+ from collections.abc import Sequence
7
+ from functools import wraps
8
+ from multiprocessing import Process, Queue
9
+ from typing import Annotated, TypedDict
10
+
11
+ from langchain_core.messages import BaseMessage, SystemMessage, ToolMessage
12
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
13
+ from langchain_core.runnables import RunnableConfig
14
+ from langgraph.graph import END, StateGraph
15
+ from langgraph.graph.message import add_messages
16
+
17
+ from biomni.config import default_config
18
+ from biomni.env_desc import data_lake_dict, library_content_dict
19
+ from biomni.llm import get_llm
20
+ from biomni.model.retriever import ToolRetriever
21
+ from biomni.tool.tool_registry import ToolRegistry
22
+ from biomni.utils import (
23
+ api_schema_to_langchain_tool,
24
+ function_to_api_schema,
25
+ pretty_print,
26
+ read_module2api,
27
+ )
28
+
29
+
30
+ # Define the AgentState TypedDict for our custom implementation
31
+ class AgentState(TypedDict):
32
+ """The state of the agent."""
33
+
34
+ # add_messages is a reducer that combines message sequences
35
+ messages: Annotated[Sequence[BaseMessage], add_messages]
36
+
37
+
38
+ class react:
39
+ def __init__(
40
+ self,
41
+ path: str | None = None,
42
+ llm: str | None = None,
43
+ use_tool_retriever: bool | None = None,
44
+ timeout_seconds: int | None = None,
45
+ ):
46
+ # Use default_config values for unspecified parameters
47
+ if path is None:
48
+ path = default_config.path
49
+ if llm is None:
50
+ llm = default_config.llm
51
+ if use_tool_retriever is None:
52
+ use_tool_retriever = default_config.use_tool_retriever
53
+ if timeout_seconds is None:
54
+ timeout_seconds = default_config.timeout_seconds
55
+
56
+ self.path = path
57
+ if not os.path.exists(path):
58
+ os.makedirs(path)
59
+ print(f"Created directory: {path}")
60
+ ### TODO: Download the data
61
+ else:
62
+ print(f"Data directory already exists: {path}, loading...")
63
+
64
+ module2api = read_module2api()
65
+
66
+ self.llm = get_llm(llm, config=default_config)
67
+ tools = []
68
+ for module, api_list in module2api.items():
69
+ print("Registering tools from module:", module)
70
+ tools += [api_schema_to_langchain_tool(api, mode="custom_tool", module_name=module) for api in api_list]
71
+ self.tools = tools
72
+ self.module2api = module2api
73
+ self.use_tool_retriever = use_tool_retriever
74
+
75
+ # Store dictionaries for data lake and library content
76
+ self.data_lake_dict = data_lake_dict
77
+ self.library_content_dict = library_content_dict
78
+
79
+ if self.use_tool_retriever:
80
+ self.tool_registry = ToolRegistry(module2api)
81
+ self.retriever = ToolRetriever()
82
+
83
+ self.timeout_seconds = timeout_seconds # 10 minutes default timeout
84
+
85
+ # When wrapping tools with timeout
86
+ self.tools = self._add_timeout_to_tools(self.tools)
87
+ self.prompt = ""
88
+ self.system_prompt = ""
89
+
90
+ def _add_timeout_to_tools(self, tools):
91
+ """Apply timeout wrapper to all tool functions using multiprocessing."""
92
+
93
+ def create_timed_func(original_func, timeout):
94
+ """Factory function that creates a unique timed function for each tool."""
95
+ tool_name = getattr(original_func, "__name__", "unknown")
96
+ # print(f"Applying timeout wrapper to tool: {tool_name}")
97
+
98
+ def process_func(func, args, kwargs, result_queue):
99
+ """Function to run in a separate process."""
100
+ try:
101
+ result = func(*args, **kwargs)
102
+ result_queue.put(("success", result))
103
+ except Exception as e:
104
+ result_queue.put(("error", str(e)))
105
+
106
+ @wraps(original_func)
107
+ def timed_func(*args, **kwargs):
108
+ # print(f"Executing tool with timeout: {tool_name}")
109
+ result_queue = Queue()
110
+
111
+ # Start a separate process
112
+ proc = Process(
113
+ target=process_func,
114
+ args=(original_func, args, kwargs, result_queue),
115
+ )
116
+ proc.start()
117
+
118
+ # Wait for the specified timeout
119
+ proc.join(timeout)
120
+
121
+ # Check if the process is still running after timeout
122
+ if proc.is_alive():
123
+ print(f"TIMEOUT: Tool {tool_name} execution timed out after {timeout} seconds")
124
+ # Force terminate the process
125
+ proc.terminate()
126
+ proc.join(1) # Give it a second to terminate
127
+
128
+ # If it's still not dead, kill it with more force
129
+ if proc.is_alive():
130
+ os.kill(proc.pid, signal.SIGKILL)
131
+
132
+ return f"ERROR: Tool execution timed out after {timeout} seconds. Please try with simpler inputs or break your task into smaller steps."
133
+
134
+ # Get the result from the queue
135
+ if not result_queue.empty():
136
+ status, result = result_queue.get()
137
+ if status == "success":
138
+ return result
139
+ else:
140
+ return f"Error in tool execution: {result}"
141
+
142
+ return "Error: Tool execution completed but no result was returned"
143
+
144
+ return timed_func
145
+
146
+ wrapped_tools = []
147
+ for tool in tools:
148
+ wrapped_tool = tool
149
+ wrapped_tool.func = create_timed_func(tool.func, self.timeout_seconds)
150
+ wrapped_tools.append(wrapped_tool)
151
+
152
+ return wrapped_tools
153
+
154
+ def add_tool(self, api):
155
+ function_code = inspect.getsource(api)
156
+ schema = function_to_api_schema(function_code, self.llm)
157
+ new_tool = api_schema_to_langchain_tool(schema, mode="custom_tool", module_name=api.__module__)
158
+
159
+ # Create a single wrapped tool using the existing _add_timeout_to_tools method
160
+ wrapped_tools = self._add_timeout_to_tools([new_tool])
161
+
162
+ # Get the wrapped tool and add it to our tools list
163
+ if wrapped_tools:
164
+ self.tools.append(wrapped_tools[0])
165
+
166
+ def configure(
167
+ self,
168
+ plan=False,
169
+ reflect=False,
170
+ data_lake=False,
171
+ react_code_search=False,
172
+ library_access=False,
173
+ ):
174
+ data_lake_path = self.path + "/data_lake"
175
+ data_lake_content = glob.glob(data_lake_path + "/*")
176
+ data_lake_items = [x.split("/")[-1] for x in data_lake_content]
177
+
178
+ if react_code_search:
179
+ tools = [i for i in self.tools if i.name in ["run_python_repl", "search_google"]]
180
+
181
+ prompt_modifier = """
182
+ You are a helpful biomedical assistant assigned with the task of problem-solving.
183
+
184
+ You have access to two tools:
185
+ 1) run_python_repl: to write and run python code
186
+ 2) search_google: to search google for information
187
+
188
+ You can use them to solve the problem.
189
+ """
190
+ else:
191
+ tools = self.tools
192
+ if (not plan) and (not reflect):
193
+ prompt_modifier = """You are a helpful biologist and expert geneticist.
194
+ """
195
+ elif plan and (not reflect):
196
+ prompt_modifier = """You are a helpful biologist and expert geneticist.
197
+ Given the question from the user,
198
+ - First, come up with a high level plan based on your understanding of the problem and available tools and record it in the Research Plan and Status. You can revise the plan later.
199
+ - Research Plan and Status should well organized and succinctly keep track of 1) high level plan (can be revised), 2) what steps have been done and what steps are in progress, 3) short results and conclusions of each step after it has been performed. Do not perform action in research plan.
200
+ - Research Plan and Status must only include progress that has been made by previous steps. It should not include results not directly confirmed by the previous observation.
201
+ - Follow the plan and try to achieve the goal as straightforwardly as possible. Use tools as necessary.
202
+ """
203
+ elif (not plan) and reflect:
204
+ prompt_modifier = """You are a helpful biologist and expert geneticist.
205
+ In each round after the tool is used, conduct "reflection" step: reflect on the current state of the problem and the results of the last round. What does the observation mean? If there is an error, what caused the error and how to debug?
206
+ """
207
+ else:
208
+ prompt_modifier = """You are a helpful biologist and expert geneticist.
209
+ Given the question from the user,
210
+ - First, come up with a high level plan based on your understanding of the problem and available tools and record it in the Research Plan and Status. You can revise the plan later.
211
+ - Research Plan and Status should well organized and succinctly keep track of 1) high level plan (can be revised), 2) what steps have been done and what steps are in progress, 3) short results and conclusions of each step after it has been performed. Do not perform action in research plan.
212
+ - Research Plan and Status must only include progress that has been made by previous steps. It should not include results not directly confirmed by the previous observation.
213
+ - Follow the plan and try to achieve the goal as straightforwardly as possible. Use tools as necessary.
214
+ In each round after the tool is used, conduct "reflection" step: reflect on the current state of the problem and the results of the last round. What does the observation mean? If there is an error, what caused the error and how to debug?
215
+ You have access to write_python_code and run_python_repl tool to write and run your own code if tools fail, or if the given tools are not enough. Please always make sure to write code when dealing with substantial data, including finding the length of long sequences or elements at different positions.
216
+ """
217
+
218
+ if data_lake:
219
+ # Format data lake items with descriptions
220
+ data_lake_formatted = []
221
+ for item in data_lake_items:
222
+ description = data_lake_dict.get(item, f"Data lake item: {item}")
223
+ data_lake_formatted.append(f"{item}: {description}")
224
+
225
+ prompt_modifier += """
226
+ You can also access a biological data lake at the following path: {data_lake_path}. You can use the run_python_repl tool to write code to understand the data, process and utilize it for the task.
227
+ Here is the list of datasets with their descriptions:
228
+ ----
229
+ {data_lake_formatted}
230
+ ----
231
+ """.format(
232
+ data_lake_path=data_lake_path,
233
+ data_lake_formatted="\n".join(data_lake_formatted),
234
+ )
235
+
236
+ if library_access:
237
+ # Format library content with descriptions
238
+ library_formatted = []
239
+ for lib_name, lib_desc in library_content_dict.items():
240
+ library_formatted.append(f"{lib_name}: {lib_desc}")
241
+
242
+ prompt_modifier += """
243
+ You also have access to a list of software packages that can be used to perform various tasks.
244
+ You can use the run_python_repl tool to write code to access and utilize the library for the task.
245
+ Don't forget the import statement.
246
+ Here is the list of available libraries with their descriptions:
247
+ ----
248
+ {library_formatted}
249
+ ----
250
+ """.format(library_formatted="\n".join(library_formatted))
251
+
252
+ print("=" * 25 + "System Prompt" + "=" * 25)
253
+ print(prompt_modifier)
254
+ self.system_prompt = prompt_modifier
255
+ self.prompt = ChatPromptTemplate.from_messages(
256
+ [
257
+ ("system", prompt_modifier),
258
+ MessagesPlaceholder(variable_name="messages"),
259
+ ]
260
+ )
261
+
262
+ # Store the tools for later use
263
+ self.active_tools = tools
264
+
265
+ # Create a custom implementation of the ReAct agent using LangGraph
266
+ self.app = self._create_custom_react_agent(self.llm, tools, self.prompt)
267
+
268
+ def _create_custom_react_agent(self, llm, tools, prompt):
269
+ """Create a custom ReAct agent using LangGraph."""
270
+ # Create a dictionary mapping tool names to tool objects for easy lookup
271
+ tools_by_name = {tool.name: tool for tool in tools}
272
+
273
+ # Bind the tools to the language model
274
+ llm_with_tools = llm.bind_tools(tools)
275
+
276
+ # Define the node that calls the model
277
+ def call_model(state: AgentState, config: RunnableConfig = None):
278
+ """Node that calls the language model to get the next action."""
279
+ system_message = SystemMessage(content=self.system_prompt)
280
+ messages = [system_message] + state["messages"]
281
+ response = llm_with_tools.invoke(messages, config=config)
282
+ return {"messages": [response]}
283
+
284
+ # Define the node that executes tools
285
+ def tool_node(state: AgentState):
286
+ """Node that executes tools based on the LLM's decisions."""
287
+ outputs = []
288
+ for tool_call in state["messages"][-1].tool_calls:
289
+ try:
290
+ tool_result = tools_by_name[tool_call["name"]].invoke(tool_call["args"])
291
+ outputs.append(
292
+ ToolMessage(
293
+ content=json.dumps(tool_result),
294
+ name=tool_call["name"],
295
+ tool_call_id=tool_call["id"],
296
+ )
297
+ )
298
+ except Exception as e:
299
+ # Handle any errors that occur during tool execution
300
+ outputs.append(
301
+ ToolMessage(
302
+ content=json.dumps({"error": str(e)}),
303
+ name=tool_call["name"],
304
+ tool_call_id=tool_call["id"],
305
+ )
306
+ )
307
+ return {"messages": outputs}
308
+
309
+ # Define the conditional edge that determines whether to continue or not
310
+ def should_continue(state: AgentState):
311
+ """Determine if we should continue running the graph or finish."""
312
+ messages = state["messages"]
313
+ last_message = messages[-1]
314
+ # If there is no tool call, then we finish
315
+ if not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
316
+ return "end"
317
+ # Otherwise if there is, we continue
318
+ else:
319
+ return "continue"
320
+
321
+ # Define a new graph
322
+ workflow = StateGraph(AgentState)
323
+
324
+ # Define the two nodes we will cycle between
325
+ workflow.add_node("agent", call_model)
326
+ workflow.add_node("tools", tool_node)
327
+
328
+ # Set the entrypoint as `agent`
329
+ workflow.set_entry_point("agent")
330
+
331
+ # Add conditional edges
332
+ workflow.add_conditional_edges(
333
+ "agent",
334
+ should_continue,
335
+ {
336
+ "continue": "tools",
337
+ "end": END,
338
+ },
339
+ )
340
+
341
+ # Add edge from tools back to agent
342
+ workflow.add_edge("tools", "agent")
343
+
344
+ # Compile the graph
345
+ return workflow.compile()
346
+
347
+ def go(self, prompt):
348
+ """Execute the agent with the given prompt.
349
+
350
+ Args:
351
+ prompt: The user's query
352
+
353
+ """
354
+ if self.use_tool_retriever:
355
+ # Gather all available tools from the registry
356
+ all_tools = self.tool_registry.tools if hasattr(self, "tool_registry") else []
357
+
358
+ # Get data lake items with descriptions
359
+ data_lake_path = self.path + "/data_lake"
360
+ data_lake_content = glob.glob(data_lake_path + "/*")
361
+ data_lake_items = [x.split("/")[-1] for x in data_lake_content]
362
+
363
+ # Create data lake descriptions for retrieval
364
+ data_lake_descriptions = []
365
+ for item in data_lake_items:
366
+ description = self.data_lake_dict.get(item, f"Data lake item: {item}")
367
+ data_lake_descriptions.append({"name": item, "description": description})
368
+
369
+ # Libraries with descriptions
370
+ library_descriptions = []
371
+ for lib_name, lib_desc in self.library_content_dict.items():
372
+ library_descriptions.append({"name": lib_name, "description": lib_desc})
373
+
374
+ # Prepare resources for retrieval
375
+ resources = {
376
+ "tools": all_tools,
377
+ "data_lake": data_lake_descriptions,
378
+ "libraries": library_descriptions,
379
+ }
380
+
381
+ # Use prompt-based retrieval with the agent's LLM
382
+ selected_resources = self.retriever.prompt_based_retrieval(prompt, resources, llm=self.llm)
383
+ print("Using prompt-based retrieval with the agent's LLM")
384
+
385
+ # If we're using prompt or embedding based retrieval, print the selected resources
386
+ print("\nSelected tools:")
387
+ for tool in selected_resources["tools"]:
388
+ if isinstance(tool, dict):
389
+ print(f"- {tool.get('name', 'Unknown')}: {tool.get('description', '')}")
390
+ else:
391
+ print(f"- {getattr(tool, 'name', str(tool))}: {getattr(tool, 'description', '')}")
392
+
393
+ print("\nSelected data lake items:")
394
+ for item in selected_resources["data_lake"]:
395
+ if isinstance(item, dict):
396
+ name = item.get("name", "Unknown")
397
+ description = self.data_lake_dict.get(name, f"Data lake item: {name}")
398
+ print(f"- {name}: {description}")
399
+ elif isinstance(item, str) and ": " in item:
400
+ # If the item already has a description, print it as is
401
+ print(f"- {item}")
402
+ else:
403
+ description = self.data_lake_dict.get(item, f"Data lake item: {item}")
404
+ print(f"- {item}: {description}")
405
+
406
+ print("\nSelected libraries:")
407
+ for lib in selected_resources["libraries"]:
408
+ if isinstance(lib, dict):
409
+ print(f"- {lib.get('name', 'Unknown')}: {lib.get('description', '')}")
410
+ else:
411
+ print(f"- {lib}")
412
+
413
+ # Convert selected tools to langchain tool objects
414
+ tool_names = [
415
+ tool["name"] if isinstance(tool, dict) else getattr(tool, "name", str(tool))
416
+ for tool in selected_resources["tools"]
417
+ ]
418
+ retrieved_list_of_tools = []
419
+
420
+ # Get the tool objects by name
421
+ for tool_name in tool_names:
422
+ # Find the tool in the original tools list
423
+ matching_tools = [t for t in self.tools if getattr(t, "name", None) == tool_name]
424
+ if matching_tools:
425
+ retrieved_list_of_tools.append(matching_tools[0])
426
+
427
+ # Add back coding tool if not already included
428
+ if len([i for i in retrieved_list_of_tools if i.name == "run_python_repl"]) == 0:
429
+ retrieved_list_of_tools = retrieved_list_of_tools + [
430
+ i for i in self.tools if i.name == "run_python_repl"
431
+ ]
432
+
433
+ print("Retrieved tools: \n" + "\n".join([l.name + ": " + l.description for l in retrieved_list_of_tools]))
434
+ # Recreate the custom agent with the retrieved tools
435
+ self.app = self._create_custom_react_agent(self.llm, retrieved_list_of_tools, self.prompt)
436
+
437
+ # Default behavior (no tool retriever or retrieval_method is 'none')
438
+ config = {"recursion_limit": 50}
439
+ inputs = {"messages": [("user", prompt)]}
440
+ self.log = []
441
+ for s in self.app.stream(inputs, stream_mode="values", config=config):
442
+ message = s["messages"][-1]
443
+ out = pretty_print(message)
444
+ self.log.append(out)
445
+ return self.log, s["messages"][-1].content
446
+
447
+ def result_formatting(self, output_class, task_intention):
448
+ self.format_check_prompt = ChatPromptTemplate.from_messages(
449
+ [
450
+ (
451
+ "system",
452
+ (
453
+ "You are evaluateGPT, tasked with extract and parse the task output based on the history of an agent. "
454
+ "Review the entire history of messages provided. "
455
+ "Here is the task output requirement: \n"
456
+ f"'{task_intention.replace('{', '{{').replace('}', '}}')}'.\n"
457
+ ),
458
+ ),
459
+ ("placeholder", "{messages}"),
460
+ ]
461
+ )
462
+
463
+ checker_llm = self.format_check_prompt | self.llm.with_structured_output(output_class)
464
+ result = checker_llm.invoke({"messages": [("user", str(self.log))]}).dict()
465
+ return result
Biomni/biomni/config.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Biomni Configuration Management
3
+
4
+ Simple configuration class for centralizing common settings.
5
+ Maintains full backward compatibility with existing code.
6
+ """
7
+
8
+ import os
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class BiomniConfig:
14
+ """Central configuration for Biomni agent.
15
+
16
+ All settings are optional and have sensible defaults.
17
+ API keys are still read from environment variables to maintain
18
+ compatibility with existing .env file structure.
19
+
20
+ Usage:
21
+ # Create config with defaults
22
+ config = BiomniConfig()
23
+
24
+ # Override specific settings
25
+ config = BiomniConfig(llm="gpt-4", timeout_seconds=1200)
26
+
27
+ # Modify after creation
28
+ config.path = "./custom_data"
29
+ """
30
+
31
+ # Data and execution settings
32
+ path: str = "./data"
33
+ timeout_seconds: int = 600
34
+
35
+ # LLM settings (API keys still from environment)
36
+ llm: str = "claude-sonnet-4-5"
37
+ temperature: float = 0.7
38
+
39
+ # Tool settings
40
+ use_tool_retriever: bool = True
41
+
42
+ # Data licensing settings
43
+ commercial_mode: bool = False # If True, excludes non-commercial datasets
44
+
45
+ # Custom model settings (for custom LLM serving)
46
+ base_url: str | None = None
47
+ api_key: str | None = None # Only for custom models, not provider API keys
48
+
49
+ # LLM source (auto-detected if None)
50
+ source: str | None = None
51
+
52
+ # Third-party integrations
53
+ protocols_io_access_token: str | None = None
54
+
55
+ def __post_init__(self):
56
+ """Load any environment variable overrides if they exist."""
57
+ # Check for environment variable overrides (optional)
58
+ # Support both old and new names for backwards compatibility
59
+ if os.getenv("BIOMNI_PATH") or os.getenv("BIOMNI_DATA_PATH"):
60
+ self.path = os.getenv("BIOMNI_PATH") or os.getenv("BIOMNI_DATA_PATH")
61
+ if os.getenv("BIOMNI_TIMEOUT_SECONDS"):
62
+ self.timeout_seconds = int(os.getenv("BIOMNI_TIMEOUT_SECONDS"))
63
+ if os.getenv("BIOMNI_LLM") or os.getenv("BIOMNI_LLM_MODEL"):
64
+ self.llm = os.getenv("BIOMNI_LLM") or os.getenv("BIOMNI_LLM_MODEL")
65
+ if os.getenv("BIOMNI_USE_TOOL_RETRIEVER"):
66
+ self.use_tool_retriever = os.getenv("BIOMNI_USE_TOOL_RETRIEVER").lower() == "true"
67
+ if os.getenv("BIOMNI_COMMERCIAL_MODE"):
68
+ self.commercial_mode = os.getenv("BIOMNI_COMMERCIAL_MODE").lower() == "true"
69
+ if os.getenv("BIOMNI_TEMPERATURE"):
70
+ self.temperature = float(os.getenv("BIOMNI_TEMPERATURE"))
71
+ if os.getenv("BIOMNI_CUSTOM_BASE_URL"):
72
+ self.base_url = os.getenv("BIOMNI_CUSTOM_BASE_URL")
73
+ if os.getenv("BIOMNI_CUSTOM_API_KEY"):
74
+ self.api_key = os.getenv("BIOMNI_CUSTOM_API_KEY")
75
+ if os.getenv("BIOMNI_SOURCE"):
76
+ self.source = os.getenv("BIOMNI_SOURCE")
77
+
78
+ # Protocols.io access token (prefer specific env vars)
79
+ env_token = os.getenv("PROTOCOLS_IO_ACCESS_TOKEN") or os.getenv("BIOMNI_PROTOCOLS_IO_ACCESS_TOKEN")
80
+ if env_token:
81
+ self.protocols_io_access_token = env_token
82
+
83
+ def to_dict(self) -> dict:
84
+ """Convert config to dictionary for easy access."""
85
+ return {
86
+ "path": self.path,
87
+ "timeout_seconds": self.timeout_seconds,
88
+ "llm": self.llm,
89
+ "temperature": self.temperature,
90
+ "use_tool_retriever": self.use_tool_retriever,
91
+ "commercial_mode": self.commercial_mode,
92
+ "base_url": self.base_url,
93
+ "api_key": self.api_key,
94
+ "source": self.source,
95
+ }
96
+
97
+
98
+ # Global default config instance (optional, for convenience)
99
+ default_config = BiomniConfig()
Biomni/biomni/env_desc.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data lake dictionary with detailed descriptions
2
+ data_lake_dict = {
3
+ "affinity_capture-ms.parquet": "Protein-protein interactions detected via affinity capture and mass spectrometry.",
4
+ "affinity_capture-rna.parquet": "Protein-RNA interactions detected by affinity capture.",
5
+ "BindingDB_All_202409.tsv": "Measured binding affinities between proteins and small molecules for drug discovery.",
6
+ "broad_repurposing_hub_molecule_with_smiles.parquet": "Molecules from Broad Institute's Drug Repurposing Hub with SMILES annotations.",
7
+ "broad_repurposing_hub_phase_moa_target_info.parquet": "Drug phases, mechanisms of action, and target information from Broad Institute.",
8
+ "co-fractionation.parquet": "Protein-protein interactions from co-fractionation experiments.",
9
+ "czi_census_datasets_v4.parquet": "Datasets from the Chan Zuckerberg Initiative's Cell Census.",
10
+ "DepMap_CRISPRGeneDependency.csv": "Gene dependency probability estimates for cancer cell lines, including all DepMap models.",
11
+ "DepMap_CRISPRGeneEffect.csv": "Genome-wide CRISPR gene effect estimates for cancer cell lines, including all DepMap models.",
12
+ "DepMap_Model.csv": "Metadata describing all cancer models/cell lines which are referenced by a dataset contained within the DepMap portal.",
13
+ "DepMap_OmicsExpressionProteinCodingGenesTPMLogp1.csv": "Gene expression in TPMs for cancer cell lines, including all DepMap models.",
14
+ "ddinter_alimentary_tract_metabolism.csv": "Drug-drug interactions for alimentary tract and metabolism drugs from DDInter 2.0 database.",
15
+ "ddinter_antineoplastic.csv": "Drug-drug interactions for antineoplastic and immunomodulating agents from DDInter 2.0 database.",
16
+ "ddinter_antiparasitic.csv": "Drug-drug interactions for antiparasitic products from DDInter 2.0 database.",
17
+ "ddinter_blood_organs.csv": "Drug-drug interactions for blood and blood forming organs drugs from DDInter 2.0 database.",
18
+ "ddinter_dermatological.csv": "Drug-drug interactions for dermatological drugs from DDInter 2.0 database.",
19
+ "ddinter_hormonal.csv": "Drug-drug interactions for systemic hormonal preparations from DDInter 2.0 database.",
20
+ "ddinter_respiratory.csv": "Drug-drug interactions for respiratory system drugs from DDInter 2.0 database.",
21
+ "ddinter_various.csv": "Drug-drug interactions for various drugs from DDInter 2.0 database.",
22
+ "DisGeNET.parquet": "Gene-disease associations from multiple sources.",
23
+ "dosage_growth_defect.parquet": "Gene dosage changes affecting growth.",
24
+ "enamine_cloud_library_smiles.pkl": "Compounds from Enamine REAL library with SMILES annotations.",
25
+ "evebio_assay_table.csv": "Assay metadata with one row per assay from EveBio pharmome mapping.",
26
+ "evebio_bundle_table.csv": "Target subfamily bundles used for screening-to-profiling progression.",
27
+ "evebio_compound_table.csv": "Compound metadata with common identifiers from EveBio screening.",
28
+ "evebio_control_table.csv": "Control datapoints for all screening and profiling plates.",
29
+ "evebio_detailed_result_table.csv": "Expanded results on evebio_summary_result_table with curve fit parameters and phase categories.",
30
+ "evebio_observed_points_table.csv": "Raw observed datapoints from all screening and profiling experiments.",
31
+ "evebio_summary_result_table.csv": "Succinct summary of results for each assay-compound combination.",
32
+ "evebio_target_table.csv": "Target metadata with common identifiers from EveBio screening.",
33
+ "genebass_missense_LC_filtered.pkl": "Filtered missense variants from GeneBass.",
34
+ "genebass_pLoF_filtered.pkl": "Predicted loss-of-function variants from GeneBass.",
35
+ "genebass_synonymous_filtered.pkl": "Filtered synonymous variants from GeneBass.",
36
+ "gene_info.parquet": "Comprehensive gene information.",
37
+ "genetic_interaction.parquet": "Genetic interactions between genes.",
38
+ "go-plus.json": "Gene ontology data for functional gene annotations.",
39
+ "gtex_tissue_gene_tpm.parquet": "Gene expression (TPM) across human tissues from GTEx.",
40
+ "gwas_catalog.pkl": "Genome-wide association studies (GWAS) results.",
41
+ "hp.obo": "Official HPO release in obographs format",
42
+ "kg.csv": "Precision medicine knowledge graph with 17,080 diseases and 4+ million relationships across biological scales.",
43
+ "marker_celltype.parquet": "Cell type marker genes for identification.",
44
+ "McPAS-TCR.parquet": "T-cell receptor sequences and specificity data from McPAS database.",
45
+ "miRDB_v6.0_results.parquet": "Predicted microRNA targets from miRDB.",
46
+ "miRTarBase_microRNA_target_interaction.parquet": "Experimentally validated microRNA-target interactions from miRTarBase.",
47
+ "miRTarBase_microRNA_target_interaction_pubmed_abtract.txt": "PubMed abstracts for microRNA-target interactions in miRTarBase.",
48
+ "miRTarBase_MicroRNA_Target_Sites.parquet": "Binding sites of microRNAs on target genes from miRTarBase.",
49
+ "mousemine_m1_positional_geneset.parquet": "Positional gene sets from MouseMine.",
50
+ "mousemine_m2_curated_geneset.parquet": "Curated gene sets from MouseMine.",
51
+ "mousemine_m3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MouseMine.",
52
+ "mousemine_m5_ontology_geneset.parquet": "Ontology-based gene sets from MouseMine.",
53
+ "mousemine_m8_celltype_signature_geneset.parquet": "Cell type signature gene sets from MouseMine.",
54
+ "mousemine_mh_hallmark_geneset.parquet": "Hallmark gene sets from MouseMine.",
55
+ "msigdb_human_c1_positional_geneset.parquet": "Human positional gene sets from MSigDB.",
56
+ "msigdb_human_c2_curated_geneset.parquet": "Curated human gene sets from MSigDB.",
57
+ "msigdb_human_c3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MSigDB.",
58
+ "msigdb_human_c3_subset_transcription_factor_targets_from_GTRD.parquet": "Transcription factor targets from GTRD/MSigDB.",
59
+ "msigdb_human_c4_computational_geneset.parquet": "Computationally derived gene sets from MSigDB.",
60
+ "msigdb_human_c5_ontology_geneset.parquet": "Ontology-based gene sets from MSigDB.",
61
+ "msigdb_human_c6_oncogenic_signature_geneset.parquet": "Oncogenic signatures from MSigDB.",
62
+ "msigdb_human_c7_immunologic_signature_geneset.parquet": "Immunologic signatures from MSigDB.",
63
+ "msigdb_human_c8_celltype_signature_geneset.parquet": "Cell type signatures from MSigDB.",
64
+ "msigdb_human_h_hallmark_geneset.parquet": "Hallmark gene sets from MSigDB.",
65
+ "omim.parquet": "Genetic disorders and associated genes from OMIM.",
66
+ "proteinatlas.tsv": "Protein expression data from Human Protein Atlas.",
67
+ "proximity_label-ms.parquet": "Protein interactions via proximity labeling and mass spectrometry.",
68
+ "reconstituted_complex.parquet": "Protein complexes reconstituted in vitro.",
69
+ "sgRNA_KO_SP_mouse.txt": "sgRNA knockout data for mouse.",
70
+ "sgRNA_KO_SP_human.txt": "sgRNA knockout data for human.",
71
+ "synthetic_growth_defect.parquet": "Synthetic growth defects from genetic interactions.",
72
+ "synthetic_lethality.parquet": "Synthetic lethal interactions.",
73
+ "synthetic_rescue.parquet": "Genetic interactions rescuing phenotypes.",
74
+ "two-hybrid.parquet": "Protein-protein interactions detected by yeast two-hybrid assays.",
75
+ "variant_table.parquet": "Annotated genetic variants table.",
76
+ "Virus-Host_PPI_P-HIPSTER_2020.parquet": "Virus-host protein-protein interactions from P-HIPSTER.",
77
+ "txgnn_name_mapping.pkl": "Name mapping for TXGNN.",
78
+ "txgnn_prediction.pkl": "Prediction data for TXGNN.",
79
+ }
80
+
81
+ # Updated library_content as a dictionary with detailed descriptions
82
+ library_content_dict = {
83
+ # === PYTHON PACKAGES ===
84
+ # Core Bioinformatics Libraries (Python)
85
+ "biopython": "[Python Package] A set of tools for biological computation including parsers for bioinformatics files, access to online services, and interfaces to common bioinformatics programs.",
86
+ "biom-format": "[Python Package] The Biological Observation Matrix (BIOM) format is designed for representing biological sample by observation contingency tables with associated metadata.",
87
+ "scanpy": "[Python Package] A scalable toolkit for analyzing single-cell gene expression data, specifically designed for large datasets using AnnData.",
88
+ "scikit-bio": "[Python Package] Data structures, algorithms, and educational resources for bioinformatics, including sequence analysis, phylogenetics, and ordination methods.",
89
+ "anndata": "[Python Package] A Python package for handling annotated data matrices in memory and on disk, primarily used for single-cell genomics data.",
90
+ "mudata": "[Python Package] A Python package for multimodal data storage and manipulation, extending AnnData to handle multiple modalities.",
91
+ "pyliftover": "[Python Package] A Python implementation of UCSC liftOver tool for converting genomic coordinates between genome assemblies.",
92
+ "biopandas": "[Python Package] A package that provides pandas DataFrames for working with molecular structures and biological data.",
93
+ "biotite": "[Python Package] A comprehensive library for computational molecular biology, providing tools for sequence analysis, structure analysis, and more.",
94
+ "lazyslide": "[Python Package] A Python framework that brings interoperable, reproducible whole slide image analysis, enabling seamless histopathology workflows from preprocessing to deep learning.",
95
+ # Genomics & Variant Analysis (Python)
96
+ "gget": "[Python Package] A toolkit for accessing genomic databases and retrieving sequences, annotations, and other genomic data.",
97
+ "lifelines": "[Python Package] A complete survival analysis library for fitting models, plotting, and statistical tests.",
98
+ # "scvi-tools": "[Python Package] A package for probabilistic modeling of single-cell omics data, including deep generative models.",
99
+ "gseapy": "[Python Package] A Python wrapper for Gene Set Enrichment Analysis (GSEA) and visualization.",
100
+ "scrublet": "[Python Package] A tool for detecting doublets in single-cell RNA-seq data.",
101
+ "cellxgene-census": "[Python Package] A tool for accessing and analyzing the CellxGene Census, a collection of single-cell datasets. To download a dataset, use the download_source_h5ad function with the dataset id as the argument (856c1b98-5727-49da-bf0f-151bdb8cb056, no .h5ad extension).",
102
+ "hyperopt": "[Python Package] A Python library for optimizing hyperparameters of machine learning algorithms.",
103
+ "scvelo": "[Python Package] A tool for RNA velocity analysis in single cells using dynamical models.",
104
+ "pysam": "[Python Package] A Python module for reading, manipulating and writing genomic data sets in SAM/BAM/VCF/BCF formats.",
105
+ "pyfaidx": "[Python Package] A Python package for efficient random access to FASTA files.",
106
+ "pyranges": "[Python Package] A Python package for interval manipulation with a pandas-like interface.",
107
+ "pybedtools": "[Python Package] A Python wrapper for Aaron Quinlan's BEDTools programs.",
108
+ # "panhumanpy": "A Python package for hierarchical, cross-tissue cell type annotation of human single-cell RNA-seq data",
109
+ # Structural Biology & Drug Discovery (Python)
110
+ "rdkit": "[Python Package] A collection of cheminformatics and machine learning tools for working with chemical structures and drug discovery.",
111
+ "deeppurpose": "[Python Package] A deep learning library for drug-target interaction prediction and virtual screening.",
112
+ "pyscreener": "[Python Package] A Python package for virtual screening of chemical compounds.",
113
+ "openbabel": "[Python Package] A chemical toolbox designed to speak the many languages of chemical data, supporting file format conversion and molecular modeling.",
114
+ "descriptastorus": "[Python Package] A library for computing molecular descriptors for machine learning applications in drug discovery.",
115
+ # "pymol": "[Python Package] A molecular visualization system for rendering and animating 3D molecular structures.",
116
+ "openmm": "[Python Package] A toolkit for molecular simulation using high-performance GPU computing.",
117
+ "pytdc": "[Python Package] A Python package for Therapeutics Data Commons, providing access to machine learning datasets for drug discovery.",
118
+ # Data Science & Statistical Analysis (Python)
119
+ "pandas": "[Python Package] A fast, powerful, and flexible data analysis and manipulation library for Python.",
120
+ "numpy": "[Python Package] The fundamental package for scientific computing with Python, providing support for arrays, matrices, and mathematical functions.",
121
+ "scipy": "[Python Package] A Python library for scientific and technical computing, including modules for optimization, linear algebra, integration, and statistics.",
122
+ "scikit-learn": "[Python Package] A machine learning library featuring various classification, regression, and clustering algorithms.",
123
+ "matplotlib": "[Python Package] A comprehensive library for creating static, animated, and interactive visualizations in Python.",
124
+ "seaborn": "[Python Package] A statistical data visualization library based on matplotlib with a high-level interface for drawing attractive statistical graphics.",
125
+ "statsmodels": "[Python Package] A Python module for statistical modeling and econometrics, including descriptive statistics and estimation of statistical models.",
126
+ "pymc3": "[Python Package] A Python package for Bayesian statistical modeling and probabilistic machine learning.",
127
+ # "pystan": "[Python Package] A Python interface to Stan, a platform for statistical modeling and high-performance statistical computation.",
128
+ "umap-learn": "[Python Package] Uniform Manifold Approximation and Projection, a dimension reduction technique.",
129
+ "faiss-cpu": "[Python Package] A library for efficient similarity search and clustering of dense vectors.",
130
+ "harmony-pytorch": "[Python Package] A PyTorch implementation of the Harmony algorithm for integrating single-cell data.",
131
+ # General Bioinformatics & Computational Utilities (Python)
132
+ "tiledb": "[Python Package] A powerful engine for storing and analyzing large-scale genomic data.",
133
+ "tiledbsoma": "[Python Package] A library for working with the SOMA (Stack of Matrices) format using TileDB.",
134
+ "h5py": "[Python Package] A Python interface to the HDF5 binary data format, allowing storage of large amounts of numerical data.",
135
+ "tqdm": "[Python Package] A fast, extensible progress bar for loops and CLI applications.",
136
+ "joblib": "[Python Package] A set of tools to provide lightweight pipelining in Python, including transparent disk-caching and parallel computing.",
137
+ "opencv-python": "[Python Package] OpenCV library for computer vision tasks, useful for image analysis in biological contexts.",
138
+ "PyPDF2": "[Python Package] A library for working with PDF files, useful for extracting text from scientific papers.",
139
+ "googlesearch-python": "[Python Package] A library for performing Google searches programmatically.",
140
+ "scikit-image": "[Python Package] A collection of algorithms for image processing in Python.",
141
+ "pymed": "[Python Package] A Python library for accessing PubMed articles.",
142
+ "arxiv": "[Python Package] A Python wrapper for the arXiv API, allowing access to scientific papers.",
143
+ "scholarly": "[Python Package] A module to retrieve author and publication information from Google Scholar.",
144
+ "cryosparc-tools": "[Python Package] Tools for working with cryoSPARC, a platform for cryo-EM data processing.",
145
+ "mageck": "[Python Package] Analysis of CRISPR screen data.",
146
+ "igraph": "[Python Package] Network analysis and visualization.",
147
+ "pyscenic": "[Python Package] Analysis of single-cell RNA-seq data and gene regulatory networks.",
148
+ "cooler": "[Python Package] Storage and analysis of Hi-C data.",
149
+ "trackpy": "[Python Package] Particle tracking in images and video.",
150
+ "nnunet": "[Python Package] A deep learning framework for biomedical image segmentation, providing a standardized approach to training and inference.",
151
+ # "flowcytometrytools": "[Python Package] Analysis and visualization of flow cytometry data.",
152
+ "cellpose": "[Python Package] Cell segmentation in microscopy images.",
153
+ "viennarna": "[Python Package] RNA secondary structure prediction.",
154
+ "PyMassSpec": "[Python Package] Mass spectrometry data analysis.",
155
+ "python-libsbml": "[Python Package] Working with SBML files for computational biology.",
156
+ "cobra": "[Python Package] Constraint-based modeling of metabolic networks.",
157
+ "reportlab": "[Python Package] Creation of PDF documents.",
158
+ "flowkit": "[Python Package] Toolkit for processing flow cytometry data.",
159
+ "hmmlearn": "[Python Package] Hidden Markov model analysis.",
160
+ "msprime": "[Python Package] Simulation of genetic variation.",
161
+ "tskit": "[Python Package] Handling tree sequences and population genetics data.",
162
+ "cyvcf2": "[Python Package] Fast parsing of VCF files.",
163
+ "pykalman": "[Python Package] Kalman filter and smoother implementation.",
164
+ "fanc": "[Python Package] Analysis of chromatin conformation data.",
165
+ "loompy": "A Python implementation of the Loom file format for efficiently storing and working with large omics datasets.",
166
+ "pyBigWig": "A Python library for accessing bigWig and bigBed files for genome browser track data.",
167
+ "pymzml": "A Python module for high-throughput bioinformatics analysis of mass spectrometry data.",
168
+ "optlang": "A Python package for modeling optimization problems symbolically.",
169
+ "FlowIO": "A Python package for reading and writing flow cytometry data files.",
170
+ "FlowUtils": "Utilities for processing and analyzing flow cytometry data.",
171
+ "arboreto": "A Python package for inferring gene regulatory networks from single-cell RNA-seq data.",
172
+ "pdbfixer": "A Python package for fixing problems in PDB files in preparation for molecular simulations.",
173
+ # === R PACKAGES ===
174
+ # Core R Packages for Data Analysis
175
+ "ggplot2": "[R Package] A system for declaratively creating graphics, based on The Grammar of Graphics. Use with subprocess.run(['Rscript', '-e', 'library(ggplot2); ...']).",
176
+ "dplyr": "[R Package] A grammar of data manipulation, providing a consistent set of verbs that help you solve the most common data manipulation challenges. Use with subprocess.",
177
+ "tidyr": "[R Package] A package that helps you create tidy data, where each column is a variable, each row is an observation, and each cell is a single value. Use with subprocess.",
178
+ "readr": "[R Package] A fast and friendly way to read rectangular data like CSV, TSV, and FWF. Use with subprocess.run(['Rscript', '-e', 'library(readr); ...']).",
179
+ "stringr": "[R Package] A cohesive set of functions designed to make working with strings as easy as possible. Use with subprocess calls.",
180
+ "Matrix": "[R Package] A package that provides classes and methods for dense and sparse matrices. Required for Seurat. Use with subprocess calls.",
181
+ # "Rcpp": "[R Package] Seamless R and C++ Integration, allowing R functions to call compiled C++ code. Use with subprocess calls.",
182
+ # "devtools": "[R Package] Tools to make developing R packages easier, including functions to install packages from GitHub. Use with subprocess calls.",
183
+ # "remotes": "[R Package] Install R packages from GitHub, GitLab, Bitbucket, or other remote repositories. Use with subprocess calls.",
184
+ # Bioinformatics R Packages
185
+ "DESeq2": "[R Package] Differential gene expression analysis based on the negative binomial distribution. Use with subprocess.run(['Rscript', '-e', 'library(DESeq2); ...']).",
186
+ "clusterProfiler": "[R Package] A package for statistical analysis and visualization of functional profiles for genes and gene clusters. Use with subprocess calls.",
187
+ # "DADA2": "[R Package] A package for modeling and correcting Illumina-sequenced amplicon errors. Use with subprocess calls.",
188
+ # "xcms": "[R Package] A package for processing and visualization of LC-MS and GC-MS data. Use with subprocess calls.",
189
+ # "FlowCore": "[R Package] Basic infrastructure for flow cytometry data. Use with subprocess calls.",
190
+ "edgeR": "[R Package] Empirical Analysis of Digital Gene Expression Data in R, for differential expression analysis. Use with subprocess calls.",
191
+ "limma": "[R Package] Linear Models for Microarray Data, for differential expression analysis. Use with subprocess calls.",
192
+ "harmony": "[R Package] A method for integrating and analyzing single-cell data across datasets. Use with subprocess calls.",
193
+ "WGCNA": "[R Package] Weighted Correlation Network Analysis for studying biological networks. Use with subprocess calls.",
194
+ # === CLI TOOLS ===
195
+ # Sequence Analysis Tools
196
+ "samtools": "[CLI Tool] A suite of programs for interacting with high-throughput sequencing data. Use with subprocess.run(['samtools', ...]).",
197
+ "bowtie2": "[CLI Tool] An ultrafast and memory-efficient tool for aligning sequencing reads to long reference sequences. Use with subprocess.run(['bowtie2', ...]).",
198
+ "bwa": "[CLI Tool] Burrows-Wheeler Aligner for mapping low-divergent sequences against a large reference genome. Use with subprocess.run(['bwa', ...]).",
199
+ "bedtools": "[CLI Tool] A powerful toolset for genome arithmetic, allowing operations like intersect, merge, count, and complement on genomic features. Use with subprocess.run(['bedtools', ...]).",
200
+ "macs2": "[CLI Tool] Model-based Analysis of ChIP-Seq data, a tool for identifying transcript factor binding sites.",
201
+ # Quality Control and Processing Tools
202
+ "fastqc": "[CLI Tool] A quality control tool for high throughput sequence data. Use with subprocess.run(['fastqc', ...]).",
203
+ "trimmomatic": "[CLI Tool] A flexible read trimming tool for Illumina NGS data. Use with subprocess.run(['trimmomatic', ...]).",
204
+ # Multiple Sequence Alignment and Phylogenetics
205
+ "mafft": "[CLI Tool] A multiple sequence alignment program for unix-like operating systems. Use with subprocess.run(['mafft', ...]).",
206
+ "Homer": "[CLI Tool] Motif discovery and next-gen sequencing analysis.",
207
+ "FastTree": "[CLI Tool] Phylogenetic trees from sequence alignments.",
208
+ "muscle": "[CLI Tool] Multiple sequence alignment tool.",
209
+ # Genetic Analysis Tools
210
+ "plink": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink', ...]).",
211
+ "plink2": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink2', ...]).",
212
+ "gcta64": "[CLI Tool] Genome-wide Complex Trait Analysis (GCTA) tool for estimating the proportion of phenotypic variance explained by genome-wide SNPs and analyzing genetic relationships. Use with subprocess.run(['gcta64', ...]).",
213
+ "iqtree2": "[CLI Tool] An efficient phylogenetic software for maximum likelihood analysis with built-in model selection and ultrafast bootstrap. Use with subprocess.run(['iqtree2', ...]).",
214
+ "ADFR": "AutoDock for Receptors suite for molecular docking and virtual screening. ",
215
+ "diamond": "A sequence aligner for protein and translated DNA searches, designed for high performance analysis of big sequence data. ",
216
+ "fcsparser": "A command-line tool for parsing and analyzing flow cytometry standard (FCS) files. ",
217
+ "plannotate": "[CLI Tool] A tool for annotating plasmid sequences with common features. ",
218
+ "vina": "[CLI Tool] An open-source program for molecular docking and virtual screening, known for its speed and accuracy improvements over AutoDock 4.",
219
+ "autosite": "[CLI Tool] A binding site detection tool used to identify potential ligand binding pockets on protein structures for molecular docking.",
220
+ "PyLabRobot": "[Python Package] A Python package for controlling liquid-handling robots and other lab automation equipment.",
221
+ }
Biomni/biomni/env_desc_cm.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data lake dictionary with detailed descriptions (Commercial Mode - Non-commercial datasets commented out)
2
+ data_lake_dict = {
3
+ "affinity_capture-ms.parquet": "Protein-protein interactions detected via affinity capture and mass spectrometry.",
4
+ "affinity_capture-rna.parquet": "Protein-RNA interactions detected by affinity capture.",
5
+ # "BindingDB_All_202409.tsv": "Measured binding affinities between proteins and small molecules for drug discovery.", # Requires commercial license
6
+ "broad_repurposing_hub_molecule_with_smiles.parquet": "Molecules from Broad Institute's Drug Repurposing Hub with SMILES annotations.",
7
+ "broad_repurposing_hub_phase_moa_target_info.parquet": "Drug phases, mechanisms of action, and target information from Broad Institute.",
8
+ "co-fractionation.parquet": "Protein-protein interactions from co-fractionation experiments.",
9
+ "czi_census_datasets_v4.parquet": "Datasets from the Chan Zuckerberg Initiative's Cell Census.",
10
+ "DepMap_CRISPRGeneDependency.csv": "Gene dependency probability estimates for cancer cell lines, including all DepMap models.",
11
+ "DepMap_CRISPRGeneEffect.csv": "Genome-wide CRISPR gene effect estimates for cancer cell lines, including all DepMap models.",
12
+ "DepMap_Model.csv": "Metadata describing all cancer models/cell lines which are referenced by a dataset contained within the DepMap portal.",
13
+ "DepMap_OmicsExpressionProteinCodingGenesTPMLogp1.csv": "Gene expression in TPMs for cancer cell lines, including all DepMap models.",
14
+ # "ddinter_alimentary_tract_metabolism.csv": "Drug-drug interactions for alimentary tract and metabolism drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
15
+ # "ddinter_antineoplastic.csv": "Drug-drug interactions for antineoplastic and immunomodulating agents from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
16
+ # "ddinter_antiparasitic.csv": "Drug-drug interactions for antiparasitic products from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
17
+ # "ddinter_blood_organs.csv": "Drug-drug interactions for blood and blood forming organs drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
18
+ # "ddinter_dermatological.csv": "Drug-drug interactions for dermatological drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
19
+ # "ddinter_hormonal.csv": "Drug-drug interactions for systemic hormonal preparations from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
20
+ # "ddinter_respiratory.csv": "Drug-drug interactions for respiratory system drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
21
+ # "ddinter_various.csv": "Drug-drug interactions for various drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
22
+ # "DisGeNET.parquet": "Gene-disease associations from multiple sources.", # CC BY-NC-SA 4.0 - Non-commercial only
23
+ "dosage_growth_defect.parquet": "Gene dosage changes affecting growth.",
24
+ # "enamine_cloud_library_smiles.pkl": "Compounds from Enamine REAL library with SMILES annotations.", # Proprietary - Requires license
25
+ # "evebio_assay_table.csv": "Assay metadata with one row per assay from EveBio pharmome mapping.", # Proprietary - Requires permission
26
+ # "evebio_bundle_table.csv": "Target subfamily bundles used for screening-to-profiling progression.", # Proprietary - Requires permission
27
+ # "evebio_compound_table.csv": "Compound metadata with common identifiers from EveBio screening.", # Proprietary - Requires permission
28
+ # "evebio_control_table.csv": "Control datapoints for all screening and profiling plates.", # Proprietary - Requires permission
29
+ # "evebio_detailed_result_table.csv": "Expanded results on evebio_summary_result_table with curve fit parameters and phase categories.", # Proprietary - Requires permission
30
+ # "evebio_observed_points_table.csv": "Raw observed datapoints from all screening and profiling experiments.", # Proprietary - Requires permission
31
+ # "evebio_summary_result_table.csv": "Succinct summary of results for each assay-compound combination.", # Proprietary - Requires permission
32
+ # "evebio_target_table.csv": "Target metadata with common identifiers from EveBio screening.", # Proprietary - Requires permission
33
+ "genebass_missense_LC_filtered.pkl": "Filtered missense variants from GeneBass.",
34
+ "genebass_pLoF_filtered.pkl": "Predicted loss-of-function variants from GeneBass.",
35
+ "genebass_synonymous_filtered.pkl": "Filtered synonymous variants from GeneBass.",
36
+ "gene_info.parquet": "Comprehensive gene information.",
37
+ "genetic_interaction.parquet": "Genetic interactions between genes.",
38
+ "go-plus.json": "Gene ontology data for functional gene annotations.",
39
+ "gtex_tissue_gene_tpm.parquet": "Gene expression (TPM) across human tissues from GTEx.",
40
+ "gwas_catalog.pkl": "Genome-wide association studies (GWAS) results.",
41
+ "hp.obo": "Official HPO release in obographs format",
42
+ "kg.csv": "Precision medicine knowledge graph with 17,080 diseases and 4+ million relationships across biological scales.",
43
+ "marker_celltype.parquet": "Cell type marker genes for identification.",
44
+ # "McPAS-TCR.parquet": "T-cell receptor sequences and specificity data from McPAS database.", # CC BY-NC-SA 4.0 - Non-commercial only
45
+ # "miRDB_v6.0_results.parquet": "Predicted microRNA targets from miRDB.", # Non-commercial use only
46
+ # "miRTarBase_microRNA_target_interaction.parquet": "Experimentally validated microRNA-target interactions from miRTarBase.", # CC BY-NC 4.0 - Non-commercial only
47
+ # "miRTarBase_microRNA_target_interaction_pubmed_abtract.txt": "PubMed abstracts for microRNA-target interactions in miRTarBase.", # CC BY-NC 4.0 - Non-commercial only
48
+ # "miRTarBase_MicroRNA_Target_Sites.parquet": "Binding sites of microRNAs on target genes from miRTarBase.", # CC BY-NC 4.0 - Non-commercial only
49
+ "mousemine_m1_positional_geneset.parquet": "Positional gene sets from MouseMine.",
50
+ "mousemine_m2_curated_geneset.parquet": "Curated gene sets from MouseMine.",
51
+ "mousemine_m3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MouseMine.",
52
+ "mousemine_m5_ontology_geneset.parquet": "Ontology-based gene sets from MouseMine.",
53
+ "mousemine_m8_celltype_signature_geneset.parquet": "Cell type signature gene sets from MouseMine.",
54
+ "mousemine_mh_hallmark_geneset.parquet": "Hallmark gene sets from MouseMine.",
55
+ # "msigdb_human_c1_positional_geneset.parquet": "Human positional gene sets from MSigDB.", # Requires commercial license
56
+ # "msigdb_human_c2_curated_geneset.parquet": "Curated human gene sets from MSigDB.", # Requires commercial license
57
+ # "msigdb_human_c3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MSigDB.", # Requires commercial license
58
+ # "msigdb_human_c3_subset_transcription_factor_targets_from_GTRD.parquet": "Transcription factor targets from GTRD/MSigDB.", # Requires commercial license
59
+ # "msigdb_human_c4_computational_geneset.parquet": "Computationally derived gene sets from MSigDB.", # Requires commercial license
60
+ # "msigdb_human_c5_ontology_geneset.parquet": "Ontology-based gene sets from MSigDB.", # Requires commercial license
61
+ # "msigdb_human_c6_oncogenic_signature_geneset.parquet": "Oncogenic signatures from MSigDB.", # Requires commercial license
62
+ # "msigdb_human_c7_immunologic_signature_geneset.parquet": "Immunologic signatures from MSigDB.", # Requires commercial license
63
+ # "msigdb_human_c8_celltype_signature_geneset.parquet": "Cell type signatures from MSigDB.", # Requires commercial license
64
+ # "msigdb_human_h_hallmark_geneset.parquet": "Hallmark gene sets from MSigDB.", # Requires commercial license
65
+ # "omim.parquet": "Genetic disorders and associated genes from OMIM.", # Requires commercial license
66
+ "proteinatlas.tsv": "Protein expression data from Human Protein Atlas.",
67
+ "proximity_label-ms.parquet": "Protein interactions via proximity labeling and mass spectrometry.",
68
+ "reconstituted_complex.parquet": "Protein complexes reconstituted in vitro.",
69
+ "sgRNA_KO_SP_mouse.txt": "sgRNA knockout data for mouse.",
70
+ "sgRNA_KO_SP_human.txt": "sgRNA knockout data for human.",
71
+ "synthetic_growth_defect.parquet": "Synthetic growth defects from genetic interactions.",
72
+ "synthetic_lethality.parquet": "Synthetic lethal interactions.",
73
+ "synthetic_rescue.parquet": "Genetic interactions rescuing phenotypes.",
74
+ "two-hybrid.parquet": "Protein-protein interactions detected by yeast two-hybrid assays.",
75
+ "variant_table.parquet": "Annotated genetic variants table.",
76
+ "Virus-Host_PPI_P-HIPSTER_2020.parquet": "Virus-host protein-protein interactions from P-HIPSTER.",
77
+ "txgnn_name_mapping.pkl": "Name mapping for TXGNN.",
78
+ "txgnn_prediction.pkl": "Prediction data for TXGNN.",
79
+ }
80
+
81
+ # Updated library_content as a dictionary with detailed descriptions
82
+ library_content_dict = {
83
+ # === PYTHON PACKAGES ===
84
+ # Core Bioinformatics Libraries (Python)
85
+ "biopython": "[Python Package] A set of tools for biological computation including parsers for bioinformatics files, access to online services, and interfaces to common bioinformatics programs.",
86
+ "biom-format": "[Python Package] The Biological Observation Matrix (BIOM) format is designed for representing biological sample by observation contingency tables with associated metadata.",
87
+ "scanpy": "[Python Package] A scalable toolkit for analyzing single-cell gene expression data, specifically designed for large datasets using AnnData.",
88
+ "scikit-bio": "[Python Package] Data structures, algorithms, and educational resources for bioinformatics, including sequence analysis, phylogenetics, and ordination methods.",
89
+ "anndata": "[Python Package] A Python package for handling annotated data matrices in memory and on disk, primarily used for single-cell genomics data.",
90
+ "mudata": "[Python Package] A Python package for multimodal data storage and manipulation, extending AnnData to handle multiple modalities.",
91
+ "pyliftover": "[Python Package] A Python implementation of UCSC liftOver tool for converting genomic coordinates between genome assemblies.",
92
+ "biopandas": "[Python Package] A package that provides pandas DataFrames for working with molecular structures and biological data.",
93
+ "biotite": "[Python Package] A comprehensive library for computational molecular biology, providing tools for sequence analysis, structure analysis, and more.",
94
+ "lazyslide": "[Python Package] A Python framework that brings interoperable, reproducible whole slide image analysis, enabling seamless histopathology workflows from preprocessing to deep learning.",
95
+ # Genomics & Variant Analysis (Python)
96
+ "gget": "[Python Package] A toolkit for accessing genomic databases and retrieving sequences, annotations, and other genomic data.",
97
+ "lifelines": "[Python Package] A complete survival analysis library for fitting models, plotting, and statistical tests.",
98
+ # "scvi-tools": "[Python Package] A package for probabilistic modeling of single-cell omics data, including deep generative models.",
99
+ "gseapy": "[Python Package] A Python wrapper for Gene Set Enrichment Analysis (GSEA) and visualization.",
100
+ "scrublet": "[Python Package] A tool for detecting doublets in single-cell RNA-seq data.",
101
+ "cellxgene-census": "[Python Package] A tool for accessing and analyzing the CellxGene Census, a collection of single-cell datasets. To download a dataset, use the download_source_h5ad function with the dataset id as the argument (856c1b98-5727-49da-bf0f-151bdb8cb056, no .h5ad extension).",
102
+ "hyperopt": "[Python Package] A Python library for optimizing hyperparameters of machine learning algorithms.",
103
+ "scvelo": "[Python Package] A tool for RNA velocity analysis in single cells using dynamical models.",
104
+ "pysam": "[Python Package] A Python module for reading, manipulating and writing genomic data sets in SAM/BAM/VCF/BCF formats.",
105
+ "pyfaidx": "[Python Package] A Python package for efficient random access to FASTA files.",
106
+ "pyranges": "[Python Package] A Python package for interval manipulation with a pandas-like interface.",
107
+ "pybedtools": "[Python Package] A Python wrapper for Aaron Quinlan's BEDTools programs.",
108
+ # "panhumanpy": "A Python package for hierarchical, cross-tissue cell type annotation of human single-cell RNA-seq data",
109
+ # Structural Biology & Drug Discovery (Python)
110
+ "rdkit": "[Python Package] A collection of cheminformatics and machine learning tools for working with chemical structures and drug discovery.",
111
+ "deeppurpose": "[Python Package] A deep learning library for drug-target interaction prediction and virtual screening.",
112
+ "pyscreener": "[Python Package] A Python package for virtual screening of chemical compounds.",
113
+ "openbabel": "[Python Package] A chemical toolbox designed to speak the many languages of chemical data, supporting file format conversion and molecular modeling.",
114
+ "descriptastorus": "[Python Package] A library for computing molecular descriptors for machine learning applications in drug discovery.",
115
+ # "pymol": "[Python Package] A molecular visualization system for rendering and animating 3D molecular structures.",
116
+ "openmm": "[Python Package] A toolkit for molecular simulation using high-performance GPU computing.",
117
+ "pytdc": "[Python Package] A Python package for Therapeutics Data Commons, providing access to machine learning datasets for drug discovery.",
118
+ # Data Science & Statistical Analysis (Python)
119
+ "pandas": "[Python Package] A fast, powerful, and flexible data analysis and manipulation library for Python.",
120
+ "numpy": "[Python Package] The fundamental package for scientific computing with Python, providing support for arrays, matrices, and mathematical functions.",
121
+ "scipy": "[Python Package] A Python library for scientific and technical computing, including modules for optimization, linear algebra, integration, and statistics.",
122
+ "scikit-learn": "[Python Package] A machine learning library featuring various classification, regression, and clustering algorithms.",
123
+ "matplotlib": "[Python Package] A comprehensive library for creating static, animated, and interactive visualizations in Python.",
124
+ "seaborn": "[Python Package] A statistical data visualization library based on matplotlib with a high-level interface for drawing attractive statistical graphics.",
125
+ "statsmodels": "[Python Package] A Python module for statistical modeling and econometrics, including descriptive statistics and estimation of statistical models.",
126
+ "pymc3": "[Python Package] A Python package for Bayesian statistical modeling and probabilistic machine learning.",
127
+ # "pystan": "[Python Package] A Python interface to Stan, a platform for statistical modeling and high-performance statistical computation.",
128
+ "umap-learn": "[Python Package] Uniform Manifold Approximation and Projection, a dimension reduction technique.",
129
+ "faiss-cpu": "[Python Package] A library for efficient similarity search and clustering of dense vectors.",
130
+ "harmony-pytorch": "[Python Package] A PyTorch implementation of the Harmony algorithm for integrating single-cell data.",
131
+ # General Bioinformatics & Computational Utilities (Python)
132
+ "tiledb": "[Python Package] A powerful engine for storing and analyzing large-scale genomic data.",
133
+ "tiledbsoma": "[Python Package] A library for working with the SOMA (Stack of Matrices) format using TileDB.",
134
+ "h5py": "[Python Package] A Python interface to the HDF5 binary data format, allowing storage of large amounts of numerical data.",
135
+ "tqdm": "[Python Package] A fast, extensible progress bar for loops and CLI applications.",
136
+ "joblib": "[Python Package] A set of tools to provide lightweight pipelining in Python, including transparent disk-caching and parallel computing.",
137
+ "opencv-python": "[Python Package] OpenCV library for computer vision tasks, useful for image analysis in biological contexts.",
138
+ "PyPDF2": "[Python Package] A library for working with PDF files, useful for extracting text from scientific papers.",
139
+ "googlesearch-python": "[Python Package] A library for performing Google searches programmatically.",
140
+ "scikit-image": "[Python Package] A collection of algorithms for image processing in Python.",
141
+ "pymed": "[Python Package] A Python library for accessing PubMed articles.",
142
+ "arxiv": "[Python Package] A Python wrapper for the arXiv API, allowing access to scientific papers.",
143
+ "scholarly": "[Python Package] A module to retrieve author and publication information from Google Scholar.",
144
+ "cryosparc-tools": "[Python Package] Tools for working with cryoSPARC, a platform for cryo-EM data processing.",
145
+ "mageck": "[Python Package] Analysis of CRISPR screen data.",
146
+ "igraph": "[Python Package] Network analysis and visualization.",
147
+ "pyscenic": "[Python Package] Analysis of single-cell RNA-seq data and gene regulatory networks.",
148
+ "cooler": "[Python Package] Storage and analysis of Hi-C data.",
149
+ "trackpy": "[Python Package] Particle tracking in images and video.",
150
+ # "flowcytometrytools": "[Python Package] Analysis and visualization of flow cytometry data.",
151
+ "cellpose": "[Python Package] Cell segmentation in microscopy images.",
152
+ "viennarna": "[Python Package] RNA secondary structure prediction.",
153
+ "PyMassSpec": "[Python Package] Mass spectrometry data analysis.",
154
+ "python-libsbml": "[Python Package] Working with SBML files for computational biology.",
155
+ "cobra": "[Python Package] Constraint-based modeling of metabolic networks.",
156
+ "reportlab": "[Python Package] Creation of PDF documents.",
157
+ "flowkit": "[Python Package] Toolkit for processing flow cytometry data.",
158
+ "hmmlearn": "[Python Package] Hidden Markov model analysis.",
159
+ "msprime": "[Python Package] Simulation of genetic variation.",
160
+ "tskit": "[Python Package] Handling tree sequences and population genetics data.",
161
+ "cyvcf2": "[Python Package] Fast parsing of VCF files.",
162
+ "pykalman": "[Python Package] Kalman filter and smoother implementation.",
163
+ "fanc": "[Python Package] Analysis of chromatin conformation data.",
164
+ "loompy": "A Python implementation of the Loom file format for efficiently storing and working with large omics datasets.",
165
+ "pyBigWig": "A Python library for accessing bigWig and bigBed files for genome browser track data.",
166
+ "pymzml": "A Python module for high-throughput bioinformatics analysis of mass spectrometry data.",
167
+ "optlang": "A Python package for modeling optimization problems symbolically.",
168
+ "FlowIO": "A Python package for reading and writing flow cytometry data files.",
169
+ "FlowUtils": "Utilities for processing and analyzing flow cytometry data.",
170
+ "arboreto": "A Python package for inferring gene regulatory networks from single-cell RNA-seq data.",
171
+ "pdbfixer": "A Python package for fixing problems in PDB files in preparation for molecular simulations.",
172
+ # === R PACKAGES ===
173
+ # Core R Packages for Data Analysis
174
+ "ggplot2": "[R Package] A system for declaratively creating graphics, based on The Grammar of Graphics. Use with subprocess.run(['Rscript', '-e', 'library(ggplot2); ...']).",
175
+ "dplyr": "[R Package] A grammar of data manipulation, providing a consistent set of verbs that help you solve the most common data manipulation challenges. Use with subprocess.",
176
+ "tidyr": "[R Package] A package that helps you create tidy data, where each column is a variable, each row is an observation, and each cell is a single value. Use with subprocess.",
177
+ "readr": "[R Package] A fast and friendly way to read rectangular data like CSV, TSV, and FWF. Use with subprocess.run(['Rscript', '-e', 'library(readr); ...']).",
178
+ "stringr": "[R Package] A cohesive set of functions designed to make working with strings as easy as possible. Use with subprocess calls.",
179
+ "Matrix": "[R Package] A package that provides classes and methods for dense and sparse matrices. Required for Seurat. Use with subprocess calls.",
180
+ # "Rcpp": "[R Package] Seamless R and C++ Integration, allowing R functions to call compiled C++ code. Use with subprocess calls.",
181
+ # "devtools": "[R Package] Tools to make developing R packages easier, including functions to install packages from GitHub. Use with subprocess calls.",
182
+ # "remotes": "[R Package] Install R packages from GitHub, GitLab, Bitbucket, or other remote repositories. Use with subprocess calls.",
183
+ # Bioinformatics R Packages
184
+ "DESeq2": "[R Package] Differential gene expression analysis based on the negative binomial distribution. Use with subprocess.run(['Rscript', '-e', 'library(DESeq2); ...']).",
185
+ "clusterProfiler": "[R Package] A package for statistical analysis and visualization of functional profiles for genes and gene clusters. Use with subprocess calls.",
186
+ # "DADA2": "[R Package] A package for modeling and correcting Illumina-sequenced amplicon errors. Use with subprocess calls.",
187
+ # "xcms": "[R Package] A package for processing and visualization of LC-MS and GC-MS data. Use with subprocess calls.",
188
+ # "FlowCore": "[R Package] Basic infrastructure for flow cytometry data. Use with subprocess calls.",
189
+ "edgeR": "[R Package] Empirical Analysis of Digital Gene Expression Data in R, for differential expression analysis. Use with subprocess calls.",
190
+ "limma": "[R Package] Linear Models for Microarray Data, for differential expression analysis. Use with subprocess calls.",
191
+ "harmony": "[R Package] A method for integrating and analyzing single-cell data across datasets. Use with subprocess calls.",
192
+ "WGCNA": "[R Package] Weighted Correlation Network Analysis for studying biological networks. Use with subprocess calls.",
193
+ # === CLI TOOLS ===
194
+ # Sequence Analysis Tools
195
+ "samtools": "[CLI Tool] A suite of programs for interacting with high-throughput sequencing data. Use with subprocess.run(['samtools', ...]).",
196
+ "bowtie2": "[CLI Tool] An ultrafast and memory-efficient tool for aligning sequencing reads to long reference sequences. Use with subprocess.run(['bowtie2', ...]).",
197
+ "bwa": "[CLI Tool] Burrows-Wheeler Aligner for mapping low-divergent sequences against a large reference genome. Use with subprocess.run(['bwa', ...]).",
198
+ "bedtools": "[CLI Tool] A powerful toolset for genome arithmetic, allowing operations like intersect, merge, count, and complement on genomic features. Use with subprocess.run(['bedtools', ...]).",
199
+ "macs2": "[CLI Tool] Model-based Analysis of ChIP-Seq data, a tool for identifying transcript factor binding sites.",
200
+ # Quality Control and Processing Tools
201
+ "fastqc": "[CLI Tool] A quality control tool for high throughput sequence data. Use with subprocess.run(['fastqc', ...]).",
202
+ "trimmomatic": "[CLI Tool] A flexible read trimming tool for Illumina NGS data. Use with subprocess.run(['trimmomatic', ...]).",
203
+ # Multiple Sequence Alignment and Phylogenetics
204
+ "mafft": "[CLI Tool] A multiple sequence alignment program for unix-like operating systems. Use with subprocess.run(['mafft', ...]).",
205
+ "Homer": "[CLI Tool] Motif discovery and next-gen sequencing analysis.",
206
+ "FastTree": "[CLI Tool] Phylogenetic trees from sequence alignments.",
207
+ "muscle": "[CLI Tool] Multiple sequence alignment tool.",
208
+ # Genetic Analysis Tools
209
+ "plink": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink', ...]).",
210
+ "plink2": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink2', ...]).",
211
+ "gcta64": "[CLI Tool] Genome-wide Complex Trait Analysis (GCTA) tool for estimating the proportion of phenotypic variance explained by genome-wide SNPs and analyzing genetic relationships. Use with subprocess.run(['gcta64', ...]).",
212
+ "iqtree2": "[CLI Tool] An efficient phylogenetic software for maximum likelihood analysis with built-in model selection and ultrafast bootstrap. Use with subprocess.run(['iqtree2', ...]).",
213
+ "ADFR": "AutoDock for Receptors suite for molecular docking and virtual screening. ",
214
+ "diamond": "A sequence aligner for protein and translated DNA searches, designed for high performance analysis of big sequence data. ",
215
+ "fcsparser": "A command-line tool for parsing and analyzing flow cytometry standard (FCS) files. ",
216
+ "plannotate": "[CLI Tool] A tool for annotating plasmid sequences with common features. ",
217
+ "vina": "[CLI Tool] An open-source program for molecular docking and virtual screening, known for its speed and accuracy improvements over AutoDock 4.",
218
+ "autosite": "[CLI Tool] A binding site detection tool used to identify potential ligand binding pockets on protein structures for molecular docking.",
219
+ }
Biomni/biomni/eval/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .biomni_eval1 import BiomniEval1
2
+
3
+ __all__ = ["BiomniEval1"]
Biomni/biomni/llm.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import TYPE_CHECKING, Literal, Optional
3
+
4
+ from langchain_core.language_models.chat_models import BaseChatModel
5
+
6
+ if TYPE_CHECKING:
7
+ from biomni.config import BiomniConfig
8
+
9
+ SourceType = Literal["OpenAI", "AzureOpenAI", "Anthropic", "Ollama", "Gemini", "Bedrock", "Groq", "Custom"]
10
+ ALLOWED_SOURCES: set[str] = set(SourceType.__args__)
11
+
12
+
13
+ def get_llm(
14
+ model: str | None = None,
15
+ temperature: float | None = None,
16
+ stop_sequences: list[str] | None = None,
17
+ source: SourceType | None = None,
18
+ base_url: str | None = None,
19
+ api_key: str | None = None,
20
+ config: Optional["BiomniConfig"] = None,
21
+ ) -> BaseChatModel:
22
+ """
23
+ Get a language model instance based on the specified model name and source.
24
+ This function supports models from OpenAI, Azure OpenAI, Anthropic, Ollama, Gemini, Bedrock, and custom model serving.
25
+ Args:
26
+ model (str): The model name to use
27
+ temperature (float): Temperature setting for generation
28
+ stop_sequences (list): Sequences that will stop generation
29
+ source (str): Source provider: "OpenAI", "AzureOpenAI", "Anthropic", "Ollama", "Gemini", "Bedrock", or "Custom"
30
+ If None, will attempt to auto-detect from model name
31
+ base_url (str): The base URL for custom model serving (e.g., "http://localhost:8000/v1"), default is None
32
+ api_key (str): The API key for the custom llm
33
+ config (BiomniConfig): Optional configuration object. If provided, unspecified parameters will use config values
34
+ """
35
+ # Use config values for any unspecified parameters
36
+ if config is not None:
37
+ if model is None:
38
+ model = config.llm_model
39
+ if temperature is None:
40
+ temperature = config.temperature
41
+ if source is None:
42
+ source = config.source
43
+ if base_url is None:
44
+ base_url = config.base_url
45
+ if api_key is None:
46
+ api_key = config.api_key or "EMPTY"
47
+
48
+ # Use defaults if still not specified
49
+ if model is None:
50
+ model = "claude-3-5-sonnet-20241022"
51
+ if temperature is None:
52
+ temperature = 0.7
53
+ if api_key is None:
54
+ api_key = "EMPTY"
55
+ # Auto-detect source from model name if not specified
56
+ if source is None:
57
+ env_source = os.getenv("LLM_SOURCE")
58
+ if env_source in ALLOWED_SOURCES:
59
+ source = env_source
60
+ else:
61
+ if model[:7] == "claude-":
62
+ source = "Anthropic"
63
+ elif model[:7] == "gpt-oss":
64
+ source = "Ollama"
65
+ elif model[:4] == "gpt-":
66
+ source = "OpenAI"
67
+ elif model.startswith("azure-"):
68
+ source = "AzureOpenAI"
69
+ elif model[:7] == "gemini-":
70
+ source = "Gemini"
71
+ elif "groq" in model.lower():
72
+ source = "Groq"
73
+ elif base_url is not None:
74
+ source = "Custom"
75
+ elif "/" in model or any(
76
+ name in model.lower()
77
+ for name in [
78
+ "llama",
79
+ "mistral",
80
+ "qwen",
81
+ "gemma",
82
+ "phi",
83
+ "dolphin",
84
+ "orca",
85
+ "vicuna",
86
+ "deepseek",
87
+ ]
88
+ ):
89
+ source = "Ollama"
90
+ elif model.startswith(
91
+ ("anthropic.claude-", "amazon.titan-", "meta.llama-", "mistral.", "cohere.", "ai21.", "us.")
92
+ ):
93
+ source = "Bedrock"
94
+ else:
95
+ raise ValueError("Unable to determine model source. Please specify 'source' parameter.")
96
+
97
+ # Create appropriate model based on source
98
+ if source == "OpenAI":
99
+ try:
100
+ from langchain_openai import ChatOpenAI
101
+ except ImportError:
102
+ raise ImportError( # noqa: B904
103
+ "langchain-openai package is required for OpenAI models. Install with: pip install langchain-openai"
104
+ )
105
+ # Newer OpenAI models (e.g., gpt-5-*) require the Responses API and may reject
106
+ # legacy Chat Completions parameters like `stop`. Force Responses API when
107
+ # using gpt-5 models to avoid 400 errors such as: "Unsupported parameter: 'stop'".
108
+ use_responses = model.startswith("gpt-5")
109
+
110
+ if use_responses:
111
+ # Define a minimal subclass that drops the `stop` field when using the
112
+ # Responses API, since certain models (gpt-5-*) reject it entirely.
113
+ class _ChatOpenAIResponsesNoStop(ChatOpenAI):
114
+ def _get_request_payload(self, input_, *, stop=None, **kwargs): # type: ignore[override]
115
+ payload = super()._get_request_payload(input_, stop=stop, **kwargs)
116
+ try:
117
+ # If this call will use the Responses API, drop `stop` to avoid 400s.
118
+ if hasattr(self, "_use_responses_api") and self._use_responses_api(payload): # type: ignore[attr-defined]
119
+ payload.pop("stop", None)
120
+ # Also drop temperature for gpt-5 models as they only support default value
121
+ payload.pop("temperature", None)
122
+ except Exception:
123
+ # Be conservative: if anything goes wrong, still remove `stop` and `temperature`.
124
+ payload.pop("stop", None)
125
+ payload.pop("temperature", None)
126
+ return payload
127
+
128
+ return _ChatOpenAIResponsesNoStop(
129
+ model=model,
130
+ temperature=1, # Set to default value for gpt-5, will be removed in payload
131
+ stop_sequences=stop_sequences,
132
+ use_responses_api=True,
133
+ output_version="v0",
134
+ )
135
+ else:
136
+ return ChatOpenAI(
137
+ model=model,
138
+ temperature=temperature,
139
+ stop_sequences=stop_sequences,
140
+ )
141
+
142
+ elif source == "AzureOpenAI":
143
+ try:
144
+ from langchain_openai import AzureChatOpenAI
145
+ except ImportError:
146
+ raise ImportError( # noqa: B904
147
+ "langchain-openai package is required for Azure OpenAI models. Install with: pip install langchain-openai"
148
+ )
149
+ API_VERSION = "2024-12-01-preview"
150
+ model = model.replace("azure-", "")
151
+ return AzureChatOpenAI(
152
+ openai_api_key=os.getenv("OPENAI_API_KEY"),
153
+ azure_endpoint=os.getenv("OPENAI_ENDPOINT"),
154
+ azure_deployment=model,
155
+ openai_api_version=API_VERSION,
156
+ temperature=temperature,
157
+ )
158
+
159
+ elif source == "Anthropic":
160
+ try:
161
+ from langchain_anthropic import ChatAnthropic
162
+ except ImportError:
163
+ raise ImportError( # noqa: B904
164
+ "langchain-anthropic package is required for Anthropic models. Install with: pip install langchain-anthropic"
165
+ )
166
+
167
+ # Ensure ANTHROPIC_API_KEY is loaded from bash_profile if not in environment
168
+ if not os.environ.get("ANTHROPIC_API_KEY"):
169
+ try:
170
+ import subprocess
171
+
172
+ result = subprocess.run(
173
+ ["bash", "-c", "source ~/.bash_profile 2>/dev/null && echo $ANTHROPIC_API_KEY"],
174
+ capture_output=True,
175
+ text=True,
176
+ timeout=5,
177
+ )
178
+ if result.stdout.strip():
179
+ os.environ["ANTHROPIC_API_KEY"] = result.stdout.strip()
180
+ print("✓ Loaded ANTHROPIC_API_KEY from ~/.bash_profile")
181
+ except Exception as e:
182
+ print(f"Note: Could not load ANTHROPIC_API_KEY from bash_profile: {e}")
183
+
184
+ return ChatAnthropic(
185
+ model=model,
186
+ temperature=temperature,
187
+ max_tokens=8192,
188
+ stop_sequences=stop_sequences,
189
+ )
190
+
191
+ elif source == "Gemini":
192
+ # If you want to use ChatGoogleGenerativeAI, you need to pass the stop sequences upon invoking the model.
193
+ # return ChatGoogleGenerativeAI(
194
+ # model=model,
195
+ # temperature=temperature,
196
+ # google_api_key=api_key,
197
+ # )
198
+ try:
199
+ from langchain_openai import ChatOpenAI
200
+ except ImportError:
201
+ raise ImportError( # noqa: B904
202
+ "langchain-openai package is required for Gemini models. Install with: pip install langchain-openai"
203
+ )
204
+ return ChatOpenAI(
205
+ model=model,
206
+ temperature=temperature,
207
+ api_key=os.getenv("GEMINI_API_KEY"),
208
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
209
+ stop_sequences=stop_sequences,
210
+ )
211
+
212
+ elif source == "Groq":
213
+ try:
214
+ from langchain_openai import ChatOpenAI
215
+ except ImportError:
216
+ raise ImportError( # noqa: B904
217
+ "langchain-openai package is required for Groq models. Install with: pip install langchain-openai"
218
+ )
219
+ return ChatOpenAI(
220
+ model=model,
221
+ temperature=temperature,
222
+ api_key=os.getenv("GROQ_API_KEY"),
223
+ base_url="https://api.groq.com/openai/v1",
224
+ stop_sequences=stop_sequences,
225
+ )
226
+
227
+ elif source == "Ollama":
228
+ try:
229
+ from langchain_ollama import ChatOllama
230
+ except ImportError:
231
+ raise ImportError( # noqa: B904
232
+ "langchain-ollama package is required for Ollama models. Install with: pip install langchain-ollama"
233
+ )
234
+ return ChatOllama(
235
+ model=model,
236
+ temperature=temperature,
237
+ )
238
+
239
+ elif source == "Bedrock":
240
+ try:
241
+ from langchain_aws import ChatBedrock
242
+ except ImportError:
243
+ raise ImportError( # noqa: B904
244
+ "langchain-aws package is required for Bedrock models. Install with: pip install langchain-aws"
245
+ )
246
+ return ChatBedrock(
247
+ model=model,
248
+ temperature=temperature,
249
+ stop_sequences=stop_sequences,
250
+ region_name=os.getenv("AWS_REGION", "us-east-1"),
251
+ )
252
+
253
+ elif source == "Custom":
254
+ try:
255
+ from langchain_openai import ChatOpenAI
256
+ except ImportError:
257
+ raise ImportError( # noqa: B904
258
+ "langchain-openai package is required for custom models. Install with: pip install langchain-openai"
259
+ )
260
+ # Custom LLM serving such as SGLang. Must expose an openai compatible API.
261
+ assert base_url is not None, "base_url must be provided for customly served LLMs"
262
+ llm = ChatOpenAI(
263
+ model=model,
264
+ temperature=temperature,
265
+ max_tokens=8192,
266
+ stop_sequences=stop_sequences,
267
+ base_url=base_url,
268
+ api_key=api_key,
269
+ )
270
+ return llm
271
+
272
+ else:
273
+ raise ValueError(
274
+ f"Invalid source: {source}. Valid options are 'OpenAI', 'AzureOpenAI', 'Anthropic', 'Gemini', 'Groq', 'Bedrock', or 'Ollama'"
275
+ )
Biomni/biomni/task/__init__.py ADDED
File without changes
Biomni/biomni/task/base_task.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class base_task:
2
+ def __init__(self):
3
+ pass
4
+
5
+ def get_example(self):
6
+ pass
7
+
8
+ def get_iterator(self):
9
+ pass
10
+
11
+ def evaluate(self):
12
+ pass
13
+
14
+ def output_class(self):
15
+ pass
16
+
17
+ def get_prompt_from_input(self, input):
18
+ return self.get_example(input)["prompt"]
Biomni/biomni/task/hle.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from biomni.task.base_task import base_task
5
+
6
+ np.random.seed(42)
7
+
8
+
9
+ def shuffle(x):
10
+ np.random.shuffle(x)
11
+ return x
12
+
13
+
14
+ class humanity_last_exam(base_task):
15
+ def __init__(self, path="./data", category="Biology/Medicine", answer_type="multipleChoice"):
16
+ if category not in [
17
+ "Other",
18
+ "Humanities/Social Science",
19
+ "Math",
20
+ "Physics",
21
+ "Computer Science/AI",
22
+ "Biology/Medicine",
23
+ "Chemistry",
24
+ "Engineering",
25
+ ]:
26
+ raise ValueError(
27
+ "category must be one of ['Other', 'Humanities/Social Science', 'Math', 'Physics', 'Computer Science/AI', 'Biology/Medicine', 'Chemistry', 'Engineering']"
28
+ )
29
+ if answer_type not in ["exactMatch", "multipleChoice"]:
30
+ raise ValueError("answer_type must be one of ['exactMatch' or 'multipleChoice']")
31
+
32
+ self.dataset = category # Store dataset type
33
+ self.answer_type = answer_type
34
+ df = pd.read_parquet(path + "/hle/test_sampled_biology_medicine.parquet")
35
+
36
+ # Extract answer choices from question text
37
+ def extract_options(question):
38
+ # Find the "Answer Choices:" section
39
+ if "Answer Choices:" not in question:
40
+ return []
41
+ choices = question.split("Answer Choices:")[1].strip()
42
+
43
+ # Split on A., B., C. etc and clean up
44
+ options = []
45
+ letters = [
46
+ "A.",
47
+ "B.",
48
+ "C.",
49
+ "D.",
50
+ "E.",
51
+ "F.",
52
+ "G.",
53
+ "H.",
54
+ "I.",
55
+ "J.",
56
+ "K.",
57
+ "L.",
58
+ "M.",
59
+ "N.",
60
+ "O.",
61
+ "P.",
62
+ "Q.",
63
+ "R.",
64
+ "S.",
65
+ "T.",
66
+ "U.",
67
+ "V.",
68
+ "W.",
69
+ "X.",
70
+ "Y.",
71
+ "Z.",
72
+ ]
73
+ for i, letter in enumerate(letters):
74
+ if letter in choices:
75
+ # Define the next letter if available
76
+ next_letter = letters[i + 1] if i + 1 < len(letters) else None
77
+
78
+ # Split between current letter and next letter
79
+ parts = choices.split(letter)[1]
80
+ if next_letter and next_letter in parts:
81
+ option = parts.split(next_letter)[0].strip()
82
+ else:
83
+ option = parts.strip()
84
+
85
+ options.append(option)
86
+ return options
87
+
88
+ def extract_question(question):
89
+ return question.split("Answer Choices:")[0].strip()
90
+
91
+ # Extract options and answers only for multiple choice questions and category is the same as the dataset
92
+ df = df[df["category"] == self.dataset]
93
+ df = df[df["answer_type"] == "multipleChoice"]
94
+ df["question_text"] = df.question
95
+ df["letter_answer"] = df["answer"].apply(lambda x: x[0])
96
+
97
+ self.query = df.question_text.values
98
+ # self.options = df.options_letters.values
99
+ self.answer = df.letter_answer.values
100
+
101
+ self.prompt = """Question: {question}"""
102
+
103
+ def get_example(self, index=None):
104
+ if index is None:
105
+ index = np.random.randint(len(self.query))
106
+
107
+ return {
108
+ "prompt": self.prompt.format(
109
+ question=self.query[index],
110
+ # options = self.options[index]
111
+ ),
112
+ "answer": self.answer[index],
113
+ }
114
+
115
+ def get_iterator(self):
116
+ for i in range(len(self.query)):
117
+ yield self.get_example(i)
118
+
119
+ def evaluate(self, response):
120
+ ## expected a list/array of symbols
121
+ from sklearn.metrics import accuracy_score
122
+
123
+ ground_truth = self.answer
124
+ response = np.array(response)
125
+
126
+ return {
127
+ "accuracy": accuracy_score(ground_truth, response),
128
+ "coverage": np.mean(response != self.refrain_label),
129
+ "refrain_ratio": np.mean(response == self.refrain_label),
130
+ "precision": accuracy_score(
131
+ ground_truth[np.where(response != self.refrain_label)],
132
+ response[np.where(response != self.refrain_label)],
133
+ ),
134
+ }
135
+
136
+ def output_class(self):
137
+ from pydantic import BaseModel, Field
138
+
139
+ class MultipleChoiceOutput(BaseModel):
140
+ """Multiple choice output."""
141
+
142
+ choice: str | None = Field(
143
+ description="Multiple choice answer. For example, if there is <answer>A</answer> in the prompt, the output should be 'A'."
144
+ )
145
+
146
+ return MultipleChoiceOutput
Biomni/biomni/task/lab_bench.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from biomni.task.base_task import base_task
5
+
6
+ np.random.seed(42)
7
+
8
+
9
+ def shuffle(x):
10
+ np.random.shuffle(x)
11
+ return x
12
+
13
+
14
+ class lab_bench(base_task):
15
+ def __init__(self, path="./data", dataset="DbQA"):
16
+ if dataset not in ["DbQA", "SeqQA"]:
17
+ raise ValueError("dataset must be one of 'DbQA', 'SeqQA'")
18
+
19
+ self.dataset = dataset # Store dataset type
20
+ df = pd.read_parquet(path + "/" + dataset + "/train-00000-of-00001_test.parquet")
21
+
22
+ self.prompt = """The following is a multiple choice question about biology.
23
+ Please answer by responding with the letter of the correct answer.
24
+
25
+ Question: {question}
26
+ Options:
27
+ {options}
28
+
29
+ You MUST include the letter of the correct answer within the following tags:
30
+ [ANSWER] and [/ANSWER]. For example, '[ANSWER]<answer>[/ANSWER]',
31
+ where <answer> is the correct letter. Always answer in exactly this format
32
+ of a single letter between the two tags, even if you are unsure.
33
+ We require this because we use automatic parsing.
34
+ """
35
+
36
+ np.random.seed(42)
37
+ df["options"] = df.apply(
38
+ lambda x: shuffle(
39
+ x.distractors.tolist() + [x.ideal] + ["Insufficient information to answer the question."]
40
+ ),
41
+ axis=1,
42
+ )
43
+ df["options_letters"] = df.options.apply(
44
+ lambda x: "\n".join([chr(ord("A") + i) + "." + item for i, item in enumerate(x)])
45
+ )
46
+ df["letter_answer"] = df.apply(
47
+ lambda x: chr(ord("A") + np.where(np.array(x.options) == x.ideal)[0][0]),
48
+ axis=1,
49
+ )
50
+ df["letter_refrain"] = df.apply(
51
+ lambda x: chr(
52
+ ord("A") + np.where(np.array(x.options) == "Insufficient information to answer the question.")[0][0]
53
+ ),
54
+ axis=1,
55
+ )
56
+
57
+ self.query = df.question.values
58
+ self.options = df.options_letters.values
59
+ self.answer = df.letter_answer.values
60
+ self.refrain_label = df.letter_refrain.values
61
+
62
+ # Store protocol information if available
63
+ self.protocol = df.protocol.values if "protocol" in df.columns else None
64
+
65
+ def get_example(self, index=None):
66
+ if index is None:
67
+ index = np.random.randint(len(self.query))
68
+
69
+ if self.dataset == "ProtocolQA" and self.protocol is not None:
70
+ return {
71
+ "prompt": self.prompt.format(
72
+ protocol=self.protocol[index],
73
+ question=self.query[index],
74
+ options=self.options[index],
75
+ ),
76
+ "answer": self.answer[index],
77
+ }
78
+ else:
79
+ return {
80
+ "prompt": self.prompt.format(question=self.query[index], options=self.options[index]),
81
+ "answer": self.answer[index],
82
+ }
83
+
84
+ def get_iterator(self):
85
+ for i in range(len(self.query)):
86
+ yield self.get_example(i)
87
+
88
+ def evaluate(self, response):
89
+ ## expected a list/array of symbols
90
+ from sklearn.metrics import accuracy_score
91
+
92
+ ground_truth = self.answer
93
+ response = np.array(response)
94
+
95
+ return {
96
+ "accuracy": accuracy_score(ground_truth, response),
97
+ "coverage": np.mean(response != self.refrain_label),
98
+ "refrain_ratio": np.mean(response == self.refrain_label),
99
+ "precision": accuracy_score(
100
+ ground_truth[np.where(response != self.refrain_label)],
101
+ response[np.where(response != self.refrain_label)],
102
+ ),
103
+ }
104
+
105
+ def output_class(self):
106
+ from pydantic import BaseModel, Field
107
+
108
+ class MultipleChoiceOutput(BaseModel):
109
+ """Multiple choice output."""
110
+
111
+ choice: str | None = Field(
112
+ description="Multiple choice answer. For example, if there is <answer>A</answer> in the prompt, the output should be 'A'."
113
+ )
114
+
115
+ return MultipleChoiceOutput
Biomni/biomni/tool/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from biomni.utils import get_tool_decorated_functions # noqa: F401
Biomni/biomni/tool/biochemistry.py ADDED
@@ -0,0 +1,1026 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def analyze_circular_dichroism_spectra(
2
+ sample_name,
3
+ sample_type,
4
+ wavelength_data,
5
+ cd_signal_data,
6
+ temperature_data=None,
7
+ thermal_cd_data=None,
8
+ output_dir="./",
9
+ ):
10
+ """Analyzes circular dichroism (CD) spectroscopy data to determine secondary structure and thermal stability.
11
+
12
+ Parameters
13
+ ----------
14
+ sample_name : str
15
+ Name of the biomolecule sample (e.g., "Znf706", "G-quadruplex")
16
+ sample_type : str
17
+ Type of biomolecule ("protein" or "nucleic_acid")
18
+ wavelength_data : list or numpy.ndarray
19
+ Wavelength values in nm for CD spectrum
20
+ cd_signal_data : list or numpy.ndarray
21
+ CD signal intensity values (typically in mdeg or Δε)
22
+ temperature_data : list or numpy.ndarray, optional
23
+ Temperature values (°C) for thermal denaturation experiment
24
+ thermal_cd_data : list or numpy.ndarray, optional
25
+ CD signal values at specific wavelength across different temperatures
26
+ output_dir : str, optional
27
+ Directory to save result files, defaults to current directory
28
+
29
+ Returns
30
+ -------
31
+ str
32
+ Research log summarizing the CD analysis steps and results
33
+
34
+ """
35
+ import os
36
+ from datetime import datetime
37
+
38
+ import numpy as np
39
+
40
+ # Initialize research log
41
+ log = f"# Circular Dichroism Analysis Report for {sample_name}\n"
42
+ log += f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
43
+ log += "## Sample Information\n"
44
+ log += f"- Sample Name: {sample_name}\n"
45
+ log += f"- Sample Type: {sample_type}\n\n"
46
+
47
+ # Convert inputs to numpy arrays if they aren't already
48
+ wavelength_data = np.array(wavelength_data)
49
+ cd_signal_data = np.array(cd_signal_data)
50
+
51
+ # Ensure output directory exists
52
+ if not os.path.exists(output_dir):
53
+ os.makedirs(output_dir)
54
+
55
+ # 1. Analyze CD spectrum for secondary structure
56
+ log += "## Secondary Structure Analysis\n"
57
+
58
+ # Different analysis approaches based on sample type
59
+ if sample_type.lower() == "protein":
60
+ # Analyze protein secondary structure based on characteristic spectral features
61
+ alpha_helix_signal = np.sum((wavelength_data >= 190) & (wavelength_data <= 195) & (cd_signal_data > 0))
62
+ beta_sheet_signal = np.sum((wavelength_data >= 215) & (wavelength_data <= 220) & (cd_signal_data < 0))
63
+ random_coil_signal = np.sum((wavelength_data >= 195) & (wavelength_data <= 200) & (cd_signal_data < 0))
64
+
65
+ # Simple classification based on signal patterns
66
+ if alpha_helix_signal > beta_sheet_signal and alpha_helix_signal > random_coil_signal:
67
+ structure = "predominantly alpha-helical"
68
+ elif beta_sheet_signal > alpha_helix_signal and beta_sheet_signal > random_coil_signal:
69
+ structure = "predominantly beta-sheet"
70
+ else:
71
+ structure = "mixed or predominantly random coil"
72
+
73
+ log += f"- The CD spectrum indicates {structure} structure for {sample_name}.\n"
74
+ log += "- Key spectral features:\n"
75
+ log += " - 190-195 nm region: associated with alpha-helical content\n"
76
+ log += " - 215-220 nm region: associated with beta-sheet content\n\n"
77
+
78
+ elif sample_type.lower() == "nucleic_acid":
79
+ # Analyze nucleic acid structure (e.g., G-quadruplex has characteristic positive peak ~295 nm)
80
+ g_quadruplex_signal = np.sum((wavelength_data >= 290) & (wavelength_data <= 300) & (cd_signal_data > 0))
81
+ b_form_signal = np.sum((wavelength_data >= 270) & (wavelength_data <= 280) & (cd_signal_data > 0))
82
+
83
+ if g_quadruplex_signal > 0:
84
+ structure = "G-quadruplex characteristics"
85
+ elif b_form_signal > 0:
86
+ structure = "B-form characteristics"
87
+ else:
88
+ structure = "non-standard structure"
89
+
90
+ log += f"- The CD spectrum indicates {structure} for {sample_name}.\n"
91
+ log += "- Key spectral features:\n"
92
+ log += " - 290-300 nm positive peak: characteristic of G-quadruplex structures\n"
93
+ log += " - 270-280 nm positive peak: characteristic of B-form DNA\n\n"
94
+
95
+ # Save spectral data results
96
+ spectral_file = os.path.join(output_dir, f"{sample_name}_cd_spectrum_analysis.txt")
97
+ with open(spectral_file, "w") as f:
98
+ f.write("Wavelength (nm)\tCD Signal\n")
99
+ for wl, signal in zip(wavelength_data, cd_signal_data, strict=False):
100
+ f.write(f"{wl:.1f}\t{signal:.4f}\n")
101
+
102
+ log += f"- Detailed spectral data saved to: {spectral_file}\n\n"
103
+
104
+ # 2. Thermal stability analysis (if temperature data provided)
105
+ if temperature_data is not None and thermal_cd_data is not None:
106
+ temperature_data = np.array(temperature_data)
107
+ thermal_cd_data = np.array(thermal_cd_data)
108
+
109
+ log += "## Thermal Stability Analysis\n"
110
+
111
+ # Simple Tm estimation (melting temperature) - find temperature at 50% unfolding
112
+ # Normalize thermal data to 0-1 range for unfolding fraction
113
+ min_signal = np.min(thermal_cd_data)
114
+ max_signal = np.max(thermal_cd_data)
115
+ unfolded_fraction = (thermal_cd_data - min_signal) / (max_signal - min_signal)
116
+
117
+ # Find the temperature closest to 50% unfolding
118
+ tm_idx = np.argmin(np.abs(unfolded_fraction - 0.5))
119
+ tm = temperature_data[tm_idx]
120
+
121
+ log += f"- Estimated melting temperature (Tm): {tm:.1f}°C\n"
122
+
123
+ # Cooperativity assessment (crude estimate based on transition steepness)
124
+ t_range = temperature_data[-1] - temperature_data[0]
125
+ transition_width = (
126
+ t_range / len(temperature_data) * np.sum((unfolded_fraction > 0.2) & (unfolded_fraction < 0.8))
127
+ )
128
+
129
+ if transition_width < 0.2 * t_range:
130
+ cooperativity = "highly cooperative (sharp transition)"
131
+ elif transition_width < 0.4 * t_range:
132
+ cooperativity = "moderately cooperative"
133
+ else:
134
+ cooperativity = "non-cooperative (broad transition)"
135
+
136
+ log += f"- Thermal transition: {cooperativity}\n"
137
+
138
+ # Save thermal denaturation data
139
+ thermal_file = os.path.join(output_dir, f"{sample_name}_thermal_denaturation.txt")
140
+ with open(thermal_file, "w") as f:
141
+ f.write("Temperature (°C)\tCD Signal\tUnfolded Fraction\n")
142
+ for temp, signal, unfold in zip(temperature_data, thermal_cd_data, unfolded_fraction, strict=False):
143
+ f.write(f"{temp:.1f}\t{signal:.4f}\t{unfold:.4f}\n")
144
+
145
+ log += f"- Thermal denaturation data saved to: {thermal_file}\n\n"
146
+
147
+ # 3. Summary and conclusions
148
+ log += "## Conclusions\n"
149
+ if sample_type.lower() == "protein":
150
+ log += f"- {sample_name} shows {structure} according to CD spectroscopy.\n"
151
+ else:
152
+ log += f"- {sample_name} exhibits {structure} according to CD spectroscopy.\n"
153
+
154
+ if temperature_data is not None:
155
+ log += f"- The molecule has a melting temperature of {tm:.1f}°C with {cooperativity}.\n"
156
+
157
+ return log
158
+
159
+
160
+ def analyze_rna_secondary_structure_features(dot_bracket_structure, sequence=None):
161
+ """Calculate numeric values for various structural features of an RNA secondary structure.
162
+
163
+ Parameters
164
+ ----------
165
+ dot_bracket_structure : str
166
+ RNA secondary structure in dot-bracket notation (e.g., "(((...)))").
167
+ Parentheses represent base pairs, dots represent unpaired bases.
168
+ sequence : str, optional
169
+ The RNA sequence corresponding to the structure. If provided,
170
+ sequence-dependent energy calculations will be performed.
171
+
172
+ Returns
173
+ -------
174
+ str
175
+ A research log summarizing the calculated structural features and analysis steps.
176
+
177
+ """
178
+ # Initialize research log
179
+ log = "# RNA Secondary Structure Feature Analysis\n\n"
180
+
181
+ # Validate input
182
+ if not all(c in "().[]{}" for c in dot_bracket_structure):
183
+ return "Error: Invalid dot-bracket notation. Use only '()', '[]', '{}', and '.'"
184
+
185
+ log += f"Input structure (length: {len(dot_bracket_structure)}): {dot_bracket_structure}\n"
186
+ if sequence:
187
+ log += f"Input sequence (length: {len(sequence)}): {sequence}\n"
188
+ if len(sequence) != len(dot_bracket_structure):
189
+ return "Error: Sequence and structure lengths do not match."
190
+
191
+ # Extract base pairs
192
+ pairs = []
193
+ stack = []
194
+
195
+ for i, char in enumerate(dot_bracket_structure):
196
+ if char in "([{":
197
+ stack.append((i, char))
198
+ elif char in ")]}":
199
+ if not stack:
200
+ return "Error: Unbalanced structure. More closing than opening brackets."
201
+
202
+ j, opening_char = stack.pop()
203
+ # Check for matching bracket types
204
+ if (
205
+ (opening_char == "(" and char != ")")
206
+ or (opening_char == "[" and char != "]")
207
+ or (opening_char == "{" and char != "}")
208
+ ):
209
+ return "Error: Mismatched bracket types. Opening and closing brackets must match."
210
+
211
+ pairs.append((j, i))
212
+
213
+ if stack:
214
+ return "Error: Unbalanced structure. More opening than closing brackets."
215
+
216
+ # Sort pairs by position
217
+ pairs.sort()
218
+
219
+ # Identify stems (consecutive base pairs)
220
+ stems = []
221
+ current_stem = []
222
+
223
+ for i, (start, end) in enumerate(pairs):
224
+ if (i == 0 or start != pairs[i - 1][0] + 1 or end != pairs[i - 1][1] - 1) and current_stem:
225
+ stems.append(current_stem)
226
+ current_stem = []
227
+ current_stem.append((start, end))
228
+
229
+ if current_stem:
230
+ stems.append(current_stem)
231
+
232
+ # Calculate stem lengths
233
+ stem_lengths = [len(stem) for stem in stems]
234
+
235
+ # Calculate loop sizes
236
+ loops = []
237
+ for i in range(len(stems)):
238
+ stem = stems[i]
239
+ last_pair = stem[-1]
240
+ next_stem_start = stems[i + 1][0][0] if i < len(stems) - 1 else len(dot_bracket_structure)
241
+ loop_size = next_stem_start - last_pair[1] - 1
242
+ if loop_size > 0:
243
+ loops.append(loop_size)
244
+
245
+ # Calculate base pair statistics
246
+ total_paired_bases = len(pairs) * 2
247
+ total_unpaired_bases = len(dot_bracket_structure) - total_paired_bases
248
+
249
+ # Calculate simplified free energy if sequence is provided
250
+ stem_energies = []
251
+ if sequence and len(stems) > 0:
252
+ # Simplified energy parameters for nearest-neighbor model
253
+ # Values are approximate and simplified for illustration
254
+ energy_params = {
255
+ "AU": -0.9,
256
+ "UA": -0.9,
257
+ "GC": -2.1,
258
+ "CG": -2.1,
259
+ "GU": -0.5,
260
+ "UG": -0.5,
261
+ }
262
+
263
+ for stem in stems:
264
+ stem_energy = 0
265
+ for start, end in stem:
266
+ if start < len(sequence) and end < len(sequence):
267
+ pair = sequence[start] + sequence[end]
268
+ stem_energy += energy_params.get(pair, 0)
269
+ stem_energies.append(stem_energy)
270
+
271
+ # Prepare results
272
+ log += "\n## Structural Features\n\n"
273
+ log += f"Total base pairs: {len(pairs)}\n"
274
+ log += f"Number of stems: {len(stems)}\n"
275
+ log += f"Longest stem length: {max(stem_lengths) if stem_lengths else 0}\n"
276
+ log += f"Average stem length: {sum(stem_lengths) / len(stem_lengths) if stem_lengths else 0:.2f}\n"
277
+ log += f"Paired bases: {total_paired_bases} ({total_paired_bases / len(dot_bracket_structure) * 100:.1f}%)\n"
278
+ log += f"Unpaired bases: {total_unpaired_bases} ({total_unpaired_bases / len(dot_bracket_structure) * 100:.1f}%)\n"
279
+
280
+ if loops:
281
+ log += f"Number of loops: {len(loops)}\n"
282
+ log += f"Average loop size: {sum(loops) / len(loops):.2f}\n"
283
+ log += f"Largest loop size: {max(loops)}\n"
284
+
285
+ if sequence and stem_energies:
286
+ log += "\n## Energy Calculations\n\n"
287
+ log += f"Total estimated free energy: {sum(stem_energies):.2f} kcal/mol\n"
288
+
289
+ if len(stems) >= 2:
290
+ log += f"Upstream stem free energy: {stem_energies[0]:.2f} kcal/mol\n"
291
+ log += f"Downstream stem free energy: {stem_energies[-1]:.2f} kcal/mol\n"
292
+
293
+ # If the first stem is the "zipper" stem
294
+ if stem_lengths and stem_lengths[0] >= 3:
295
+ log += f"Zipper stem free energy: {stem_energies[0]:.2f} kcal/mol\n"
296
+
297
+ log += "\n## Stem Details\n\n"
298
+ for i, stem in enumerate(stems):
299
+ log += f"Stem {i + 1}: {len(stem)} base pairs\n"
300
+ log += f" Positions: {stem[0][0]}-{stem[0][1]} to {stem[-1][0]}-{stem[-1][1]}\n"
301
+ if sequence and i < len(stem_energies):
302
+ log += f" Estimated stability: {stem_energies[i]:.2f} kcal/mol\n"
303
+
304
+ return log
305
+
306
+
307
+ def analyze_protease_kinetics(
308
+ time_points,
309
+ fluorescence_data,
310
+ substrate_concentrations,
311
+ enzyme_concentration,
312
+ output_prefix="protease_kinetics",
313
+ output_dir="./",
314
+ ):
315
+ """Analyze protease kinetics data from fluorogenic peptide cleavage assays.
316
+
317
+ This function processes time-course fluorescence data from protease-mediated peptide
318
+ cleavage assays, fits the data to Michaelis-Menten kinetics, and determines key
319
+ kinetic parameters (kcat, KM, and catalytic efficiency).
320
+
321
+ Parameters
322
+ ----------
323
+ time_points : numpy.ndarray
324
+ Array of time points (in seconds) at which measurements were taken
325
+
326
+ fluorescence_data : numpy.ndarray
327
+ 2D array of fluorescence measurements where each row corresponds to a different
328
+ substrate concentration and each column corresponds to a time point
329
+
330
+ substrate_concentrations : numpy.ndarray
331
+ Array of substrate concentrations (in μM) corresponding to each row in fluorescence_data
332
+
333
+ enzyme_concentration : float
334
+ Concentration of the protease enzyme (in μM)
335
+
336
+ output_prefix : str, optional
337
+ Prefix for output files (default: "protease_kinetics")
338
+
339
+ output_dir : str, optional
340
+ Directory to save output files (default: "./")
341
+
342
+ Returns
343
+ -------
344
+ str
345
+ A research log summarizing the analysis steps and results
346
+
347
+ """
348
+ import os
349
+
350
+ import matplotlib.pyplot as plt
351
+ import numpy as np
352
+ from scipy.optimize import curve_fit
353
+
354
+ # Ensure output directory exists
355
+ if not os.path.exists(output_dir):
356
+ os.makedirs(output_dir)
357
+
358
+ # Create full output paths
359
+ plot_filename = os.path.join(output_dir, f"{output_prefix}_mm_plot.png")
360
+ results_filename = os.path.join(output_dir, f"{output_prefix}_results.txt")
361
+
362
+ # Step 1: Calculate initial velocities for each substrate concentration
363
+ initial_velocities = np.zeros(len(substrate_concentrations))
364
+
365
+ for i, fluorescence_curve in enumerate(fluorescence_data):
366
+ # Linear fit to the initial portion of the curve (first 20% of time points or at least 5 points)
367
+ num_points = max(5, int(len(time_points) * 0.2))
368
+ slope, _ = np.polyfit(time_points[:num_points], fluorescence_curve[:num_points], 1)
369
+ initial_velocities[i] = slope
370
+
371
+ # Step 2: Define Michaelis-Menten equation for curve fitting
372
+ def michaelis_menten(s, vmax, km):
373
+ return vmax * s / (km + s)
374
+
375
+ # Step 3: Fit the data to the Michaelis-Menten equation
376
+ try:
377
+ params, covariance = curve_fit(
378
+ michaelis_menten,
379
+ substrate_concentrations,
380
+ initial_velocities,
381
+ p0=[max(initial_velocities), np.mean(substrate_concentrations)],
382
+ bounds=([0, 0], [np.inf, np.inf]),
383
+ )
384
+
385
+ vmax, km = params
386
+ std_dev = np.sqrt(np.diag(covariance))
387
+ vmax_std, km_std = std_dev
388
+
389
+ # Step 4: Calculate kcat and catalytic efficiency
390
+ kcat = vmax / enzyme_concentration
391
+ kcat_std = vmax_std / enzyme_concentration
392
+ catalytic_efficiency = kcat / km
393
+ catalytic_efficiency_std = catalytic_efficiency * np.sqrt((kcat_std / kcat) ** 2 + (km_std / km) ** 2)
394
+
395
+ # Step 5: Create a plot and save it
396
+ plt.figure(figsize=(10, 6))
397
+ plt.scatter(
398
+ substrate_concentrations,
399
+ initial_velocities,
400
+ color="blue",
401
+ label="Experimental data",
402
+ )
403
+
404
+ # Generate smooth curve for the fitted model
405
+ s_curve = np.linspace(0, max(substrate_concentrations) * 1.2, 100)
406
+ v_curve = michaelis_menten(s_curve, vmax, km)
407
+ plt.plot(s_curve, v_curve, "r-", label="Michaelis-Menten fit")
408
+
409
+ plt.xlabel("Substrate Concentration (μM)")
410
+ plt.ylabel("Initial Velocity (a.u./s)")
411
+ plt.title("Michaelis-Menten Kinetics")
412
+ plt.legend()
413
+ plt.grid(True, alpha=0.3)
414
+
415
+ plt.savefig(plot_filename)
416
+ plt.close()
417
+
418
+ # Step 6: Save numerical results to a file
419
+ with open(results_filename, "w") as f:
420
+ f.write("Protease Kinetics Analysis Results\n")
421
+ f.write("==================================\n\n")
422
+ f.write(f"Vmax: {vmax:.4f} ± {vmax_std:.4f} a.u./s\n")
423
+ f.write(f"KM: {km:.4f} ± {km_std:.4f} μM\n")
424
+ f.write(f"kcat: {kcat:.4f} ± {kcat_std:.4f} s^-1\n")
425
+ f.write(
426
+ f"Catalytic efficiency (kcat/KM): {catalytic_efficiency:.4f} ± {catalytic_efficiency_std:.4f} μM^-1 s^-1\n"
427
+ )
428
+
429
+ # Step 7: Create research log
430
+ research_log = f"""
431
+ Protease Kinetics Analysis Research Log
432
+ ======================================
433
+
434
+ Analysis Steps:
435
+ 1. Calculated initial velocities from time-course fluorescence data for {len(substrate_concentrations)} different substrate concentrations
436
+ 2. Fitted initial velocities to the Michaelis-Menten equation using non-linear regression
437
+ 3. Determined kinetic parameters and their uncertainties
438
+
439
+ Results:
440
+ - Vmax: {vmax:.4f} ± {vmax_std:.4f} a.u./s
441
+ - KM: {km:.4f} ± {km_std:.4f} μM
442
+ - kcat: {kcat:.4f} ± {kcat_std:.4f} s^-1
443
+ - Catalytic efficiency (kcat/KM): {catalytic_efficiency:.4f} ± {catalytic_efficiency_std:.4f} μM^-1 s^-1
444
+
445
+ Files Generated:
446
+ 1. {plot_filename} - Michaelis-Menten plot showing experimental data and fitted curve
447
+ 2. {results_filename} - Text file containing detailed results
448
+
449
+ Analysis completed successfully.
450
+ """
451
+
452
+ return research_log
453
+
454
+ except Exception as e:
455
+ return f"Error during analysis: {str(e)}"
456
+
457
+
458
+ def analyze_enzyme_kinetics_assay(
459
+ enzyme_name,
460
+ substrate_concentrations,
461
+ enzyme_concentration,
462
+ modulators=None,
463
+ time_points=None,
464
+ output_dir="./",
465
+ ):
466
+ """Performs in vitro enzyme kinetics assay and analyzes the dose-dependent effects of modulators.
467
+
468
+ Parameters
469
+ ----------
470
+ enzyme_name : str
471
+ Name of the purified enzyme being tested
472
+ substrate_concentrations : list or numpy.ndarray
473
+ List of substrate concentrations in μM for kinetic analysis
474
+ enzyme_concentration : float
475
+ Concentration of the enzyme in nM
476
+ modulators : dict, optional
477
+ Dictionary of modulators where keys are modulator names and values are lists of
478
+ concentrations in μM. Default is None (no modulators).
479
+ time_points : list or numpy.ndarray, optional
480
+ Time points in minutes for time-course measurements. Default is None, which uses
481
+ [0, 5, 10, 15, 20, 30, 45, 60].
482
+ output_dir : str, optional
483
+ Directory to save output files. Default is current directory.
484
+
485
+ Returns
486
+ -------
487
+ str
488
+ Research log summarizing the enzyme kinetics assay procedure and results
489
+
490
+ """
491
+ import csv
492
+ import os
493
+
494
+ import numpy as np
495
+ from scipy.optimize import curve_fit
496
+
497
+ # Create output directory if it doesn't exist
498
+ if not os.path.exists(output_dir):
499
+ os.makedirs(output_dir)
500
+
501
+ # Set default time points if not provided
502
+ if time_points is None:
503
+ time_points = np.array([0, 5, 10, 15, 20, 30, 45, 60])
504
+ else:
505
+ # Ensure time_points is a numpy array
506
+ time_points = np.array(time_points)
507
+
508
+ # Initialize research log
509
+ log = f"## In Vitro Enzyme Kinetics Assay: {enzyme_name}\n\n"
510
+ log += f"Enzyme concentration: {enzyme_concentration} nM\n"
511
+
512
+ # Michaelis-Menten equation for curve fitting
513
+ def michaelis_menten(s, vmax, km):
514
+ return vmax * s / (km + s)
515
+
516
+ # 1. Time-course kinetic assay
517
+ log += "\n### Time-Course Kinetic Assay\n\n"
518
+ log += "Measuring enzyme activity over time to establish linear range.\n"
519
+
520
+ # Simulate time-course data (realistic enzyme kinetics with some noise)
521
+ # Using a simple exponential approach to equilibrium model
522
+ max_activity = 100 # arbitrary units
523
+ rate_constant = 0.05 # min^-1
524
+
525
+ # Simulate enzyme activity over time with some noise
526
+ np.random.seed(42) # For reproducibility
527
+ time_course_activity = max_activity * (1 - np.exp(-rate_constant * time_points))
528
+ time_course_activity += np.random.normal(0, 3, len(time_points)) # Add noise
529
+
530
+ # Save time-course data
531
+ time_course_file = os.path.join(output_dir, f"{enzyme_name}_time_course.csv")
532
+ with open(time_course_file, "w", newline="") as f:
533
+ writer = csv.writer(f)
534
+ writer.writerow(["Time (min)", "Activity (units)"])
535
+ for t, a in zip(time_points, time_course_activity, strict=False):
536
+ writer.writerow([t, a])
537
+
538
+ # Determine linear range (first ~30% of the curve)
539
+ linear_cutoff_index = np.where(time_course_activity >= 0.3 * max_activity)[0][0]
540
+ linear_time = time_points[: linear_cutoff_index + 1]
541
+
542
+ log += f"Time-course data saved to: {time_course_file}\n"
543
+ log += f"Linear range determined to be 0-{linear_time[-1]} minutes.\n"
544
+
545
+ # 2. Substrate kinetics (Michaelis-Menten analysis)
546
+ log += "\n### Substrate Kinetics Analysis\n\n"
547
+
548
+ # Simulate enzyme activity at different substrate concentrations
549
+ # based on Michaelis-Menten kinetics
550
+ true_vmax = 120 # arbitrary units
551
+ true_km = 25 # μM
552
+
553
+ activity_values = michaelis_menten(np.array(substrate_concentrations), true_vmax, true_km)
554
+ activity_values += np.random.normal(0, 5, len(substrate_concentrations)) # Add noise
555
+
556
+ # Fit data to Michaelis-Menten equation
557
+ try:
558
+ params, _ = curve_fit(
559
+ michaelis_menten,
560
+ substrate_concentrations,
561
+ activity_values,
562
+ p0=[100, 20],
563
+ bounds=([0, 0], [500, 200]),
564
+ )
565
+ vmax, km = params
566
+
567
+ # Save substrate kinetics data
568
+ kinetics_file = os.path.join(output_dir, f"{enzyme_name}_substrate_kinetics.csv")
569
+ with open(kinetics_file, "w", newline="") as f:
570
+ writer = csv.writer(f)
571
+ writer.writerow(["Substrate (μM)", "Activity (units)"])
572
+ for s, a in zip(substrate_concentrations, activity_values, strict=False):
573
+ writer.writerow([s, a])
574
+
575
+ log += "Michaelis-Menten parameters:\n"
576
+ log += f"- Vmax: {vmax:.2f} units\n"
577
+ log += f"- Km: {km:.2f} μM\n"
578
+ log += f"Substrate kinetics data saved to: {kinetics_file}\n"
579
+ except Exception:
580
+ log += "Error: Could not fit data to Michaelis-Menten model.\n"
581
+
582
+ # 3. Modulator effects (if provided)
583
+ if modulators:
584
+ log += "\n### Modulator Effects Analysis\n\n"
585
+
586
+ for modulator_name, concentrations in modulators.items():
587
+ log += f"#### Testing modulator: {modulator_name}\n\n"
588
+
589
+ # Choose a fixed substrate concentration near Km for inhibition studies
590
+ substrate_conc = true_km
591
+
592
+ # Simulate modulator effects (e.g., competitive inhibition)
593
+ # For simplicity, using a sigmoidal dose-response curve
594
+ ic50 = np.random.uniform(1, 50) # Random IC50 between 1-50 μM
595
+ hill_coef = 1.0 # Hill coefficient
596
+
597
+ # Calculate normalized activity (% of control)
598
+ michaelis_menten(substrate_conc, true_vmax, true_km)
599
+
600
+ # Calculate activities at different modulator concentrations
601
+ modulator_activities = []
602
+ for conc in concentrations:
603
+ # Sigmoidal dose-response curve
604
+ if conc == 0:
605
+ activity = 100 # 100% activity at zero concentration
606
+ else:
607
+ activity = 100 / (1 + (conc / ic50) ** hill_coef)
608
+
609
+ # Add some noise
610
+ activity += np.random.normal(0, 3)
611
+ modulator_activities.append(activity)
612
+
613
+ # Save modulator data
614
+ modulator_file = os.path.join(output_dir, f"{enzyme_name}_{modulator_name}_dose_response.csv")
615
+ with open(modulator_file, "w", newline="") as f:
616
+ writer = csv.writer(f)
617
+ writer.writerow([f"{modulator_name} (μM)", "Activity (% of control)"])
618
+ for conc, act in zip(concentrations, modulator_activities, strict=False):
619
+ writer.writerow([conc, act])
620
+
621
+ # Calculate IC50 using curve fitting if there are enough data points
622
+ if len(concentrations) >= 4:
623
+ try:
624
+ # Define dose-response curve function
625
+ def dose_response(x, ic50, hill):
626
+ return 100 / (1 + (x / ic50) ** hill)
627
+
628
+ # Filter out zero concentration
629
+ nonzero_conc = np.array([c for c in concentrations if c > 0])
630
+ nonzero_act = np.array(
631
+ [a for c, a in zip(concentrations, modulator_activities, strict=False) if c > 0]
632
+ )
633
+
634
+ if len(nonzero_conc) >= 3: # Need at least 3 points for fitting
635
+ params, _ = curve_fit(
636
+ dose_response,
637
+ nonzero_conc,
638
+ nonzero_act,
639
+ p0=[10, 1],
640
+ bounds=([0.1, 0.1], [1000, 10]),
641
+ )
642
+ calc_ic50, calc_hill = params
643
+
644
+ log += f"Dose-response analysis for {modulator_name}:\n"
645
+ log += f"- IC50: {calc_ic50:.2f} μM\n"
646
+ log += f"- Hill coefficient: {calc_hill:.2f}\n"
647
+ else:
648
+ log += f"Insufficient non-zero data points to calculate IC50 for {modulator_name}.\n"
649
+ except Exception:
650
+ log += f"Error: Could not fit dose-response curve for {modulator_name}.\n"
651
+ else:
652
+ log += f"Insufficient data points to calculate IC50 for {modulator_name}.\n"
653
+
654
+ log += f"Dose-response data for {modulator_name} saved to: {modulator_file}\n\n"
655
+
656
+ # 4. Summary
657
+ log += "\n### Summary\n\n"
658
+ log += f"Completed in vitro enzyme kinetics assay for {enzyme_name}.\n"
659
+ log += f"- Determined linear range for time-course measurements: 0-{linear_time[-1]} minutes\n"
660
+ log += f"- Characterized Michaelis-Menten kinetics (Vmax: {vmax:.2f} units, Km: {km:.2f} μM)\n"
661
+
662
+ if modulators:
663
+ log += "- Analyzed the effects of modulators:\n"
664
+ for modulator_name in modulators:
665
+ log += f" * {modulator_name}: Data saved to {enzyme_name}_{modulator_name}_dose_response.csv\n"
666
+
667
+ return log
668
+
669
+
670
+ def analyze_itc_binding_thermodynamics(
671
+ itc_data_path=None,
672
+ itc_data=None,
673
+ temperature=298.15,
674
+ protein_concentration=None,
675
+ ligand_concentration=None,
676
+ ):
677
+ """Analyzes isothermal titration calorimetry (ITC) data to determine binding affinity and thermodynamic parameters.
678
+
679
+ Parameters
680
+ ----------
681
+ itc_data_path : str, optional
682
+ Path to CSV or TSV file containing ITC thermogram data with columns for injection number,
683
+ injection volume, and heat released/absorbed. Expected columns: 'injection', 'volume', 'heat'
684
+ itc_data : numpy.ndarray, optional
685
+ Raw ITC thermogram data as a numpy array.
686
+ Expected shape: (n_injections, 3) with columns for injection number, injection volume, and heat
687
+ This parameter is provided for backward compatibility and will be deprecated.
688
+ temperature : float, optional
689
+ Temperature in Kelvin at which the experiment was conducted. Default is 298.15 K (25°C).
690
+ protein_concentration : float, optional
691
+ Initial concentration of protein in the cell in molar (M). Required for accurate fitting.
692
+ ligand_concentration : float, optional
693
+ Concentration of ligand in the syringe in molar (M). Required for accurate fitting.
694
+
695
+ Returns
696
+ -------
697
+ str
698
+ A research log summarizing the analysis steps and results, including binding affinity (Kd),
699
+ binding enthalpy (ΔH), binding entropy (ΔS), and Gibbs free energy (ΔG).
700
+
701
+ """
702
+ import datetime
703
+
704
+ import numpy as np
705
+ import pandas as pd
706
+ from scipy.optimize import curve_fit
707
+
708
+ log = []
709
+ log.append(f"# ITC Binding Affinity Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}")
710
+ log.append("\n## Data Preprocessing")
711
+
712
+ # Check if we have data to process
713
+ if itc_data_path is None and itc_data is None:
714
+ log.append("Error: No data provided. Please provide either itc_data_path or itc_data.")
715
+ return "\n".join(log)
716
+
717
+ # Load data from file if path is provided
718
+ if itc_data_path is not None:
719
+ try:
720
+ if itc_data_path.endswith(".csv"):
721
+ loaded_data = pd.read_csv(itc_data_path)
722
+ elif itc_data_path.endswith((".tsv", ".txt")):
723
+ loaded_data = pd.read_csv(itc_data_path, sep="\t")
724
+ else:
725
+ log.append("Error: Unsupported file format. Please provide a CSV or TSV file.")
726
+ return "\n".join(log)
727
+
728
+ log.append(f"- Loaded data from file: {itc_data_path}")
729
+ log.append(f"- Input data: DataFrame with {len(loaded_data)} injections")
730
+
731
+ if all(col in loaded_data.columns for col in ["injection", "volume", "heat"]):
732
+ data = loaded_data[["injection", "volume", "heat"]].values
733
+ else:
734
+ log.append("- Error: DataFrame must contain 'injection', 'volume', and 'heat' columns")
735
+ return "\n".join(log)
736
+ except Exception as e:
737
+ log.append(f"- Error loading data from file: {str(e)}")
738
+ return "\n".join(log)
739
+ # Use provided numpy array data
740
+ elif itc_data is not None:
741
+ if isinstance(itc_data, pd.DataFrame):
742
+ log.append("Warning: Passing DataFrame directly is deprecated. Please use itc_data_path instead.")
743
+ log.append(f"- Input data: DataFrame with {len(itc_data)} injections")
744
+ if all(col in itc_data.columns for col in ["injection", "volume", "heat"]):
745
+ data = itc_data[["injection", "volume", "heat"]].values
746
+ else:
747
+ log.append("- Error: DataFrame must contain 'injection', 'volume', and 'heat' columns")
748
+ return "\n".join(log)
749
+ else:
750
+ log.append(f"- Input data: Array with {len(itc_data)} injections")
751
+ data = np.array(itc_data)
752
+
753
+ # Check if concentration data is provided
754
+ if protein_concentration is None or ligand_concentration is None:
755
+ log.append("- Warning: Protein or ligand concentration not provided. Using normalized data for fitting.")
756
+ protein_concentration = 1.0
757
+ ligand_concentration = 10.0
758
+
759
+ # Extract data
760
+ injections = data[:, 0]
761
+ volumes = data[:, 1]
762
+ heats = data[:, 2]
763
+
764
+ log.append(f"- Processed {len(injections)} injections")
765
+
766
+ # Calculate molar ratio for each injection
767
+ cell_volume = 1.4 # Default cell volume in mL (typical for ITC)
768
+
769
+ # Calculate cumulative ligand added
770
+ cumulative_volume = np.cumsum(volumes)
771
+ dilution_factor = 1 - (cumulative_volume / cell_volume)
772
+ protein_conc_corrected = protein_concentration * dilution_factor
773
+
774
+ # Calculate molar ratio [ligand]/[protein] for each injection
775
+ molar_ratios = np.zeros_like(injections, dtype=float)
776
+ for i in range(len(injections)):
777
+ ligand_added = volumes[i] * ligand_concentration / 1000 # Convert to mmol
778
+ if i == 0:
779
+ molar_ratios[i] = ligand_added / (protein_concentration * cell_volume / 1000) # Convert to mmol
780
+ else:
781
+ # Account for dilution and previous injections
782
+ molar_ratios[i] = molar_ratios[i - 1] + ligand_added / (
783
+ protein_conc_corrected[i] * (cell_volume - cumulative_volume[i]) / 1000
784
+ )
785
+
786
+ log.append("\n## Model Fitting")
787
+ log.append("- Applying one-site binding model to the ITC data")
788
+
789
+ # Define one-site binding model function
790
+ def one_site_model(x, Kd, dH, n):
791
+ """One-site binding model for ITC data.
792
+
793
+ Parameters
794
+ ----------
795
+ x: Molar ratio [ligand]/[protein]
796
+ Kd: Dissociation constant (M)
797
+ dH: Enthalpy change (cal/mol)
798
+ n: Stoichiometry
799
+
800
+ Returns: Heat per injection
801
+
802
+ """
803
+ # Convert x to fraction bound using the binding equation
804
+ Ka = 1 / Kd # Association constant
805
+ protein = protein_conc_corrected
806
+
807
+ # Calculate heat for each injection
808
+ q = np.zeros_like(x)
809
+ for i in range(len(x)):
810
+ if i == 0:
811
+ # First injection
812
+ bound = (n * protein[i] * Ka * (x[i] * protein[i])) / (1 + Ka * (x[i] * protein[i]))
813
+ q[i] = bound * dH * cell_volume
814
+ else:
815
+ # Subsequent injections (differential heat)
816
+ bound_prev = (n * protein[i - 1] * Ka * (x[i - 1] * protein[i - 1])) / (
817
+ 1 + Ka * (x[i - 1] * protein[i - 1])
818
+ )
819
+ bound_curr = (n * protein[i] * Ka * (x[i] * protein[i])) / (1 + Ka * (x[i] * protein[i]))
820
+ q[i] = bound_curr * dH * (cell_volume - cumulative_volume[i]) - bound_prev * dH * (
821
+ cell_volume - cumulative_volume[i - 1]
822
+ )
823
+
824
+ return q
825
+
826
+ # Initial parameter guesses
827
+ p0 = [1e-6, -5000, 1.0] # Kd (M), dH (cal/mol), n (stoichiometry)
828
+
829
+ try:
830
+ # Fit the model to the data
831
+ popt, pcov = curve_fit(one_site_model, molar_ratios, heats, p0=p0, maxfev=10000)
832
+ Kd, dH, n = popt
833
+
834
+ # Calculate standard errors
835
+ perr = np.sqrt(np.diag(pcov))
836
+ Kd_err, dH_err, n_err = perr
837
+
838
+ # Calculate other thermodynamic parameters
839
+ R = 1.9872 # Gas constant in cal/(mol·K)
840
+ dG = R * temperature * np.log(Kd) # Gibbs free energy
841
+ dS = (dH - dG) / temperature # Entropy
842
+
843
+ log.append("- Model fitting successful")
844
+ log.append("\n## Results")
845
+ log.append(f"- Binding Stoichiometry (n): {n:.2f} ± {n_err:.2f}")
846
+ log.append(f"- Dissociation Constant (Kd): {Kd * 1e6:.2f} ± {Kd_err * 1e6:.2f} μM")
847
+ log.append(f"- Association Constant (Ka): {1 / Kd / 1e6:.2f} × 10^6 M^-1")
848
+ log.append(f"- Binding Enthalpy (ΔH): {dH:.2f} ± {dH_err:.2f} cal/mol")
849
+ log.append(f"- Binding Entropy (ΔS): {dS:.2f} cal/(mol·K)")
850
+ log.append(f"- Gibbs Free Energy (ΔG): {dG:.2f} cal/mol")
851
+
852
+ # Calculate goodness of fit
853
+ residuals = heats - one_site_model(molar_ratios, *popt)
854
+ ss_res = np.sum(residuals**2)
855
+ ss_tot = np.sum((heats - np.mean(heats)) ** 2)
856
+ r_squared = 1 - (ss_res / ss_tot)
857
+ log.append(f"- R-squared: {r_squared:.4f}")
858
+
859
+ except Exception as e:
860
+ log.append(f"- Error during model fitting: {str(e)}")
861
+ log.append("- Consider trying different initial parameter guesses or a different binding model")
862
+
863
+ log.append("\n## Conclusion")
864
+ log.append("- Analysis complete. The thermodynamic parameters have been estimated using a one-site binding model.")
865
+ log.append(
866
+ "- For more complex binding scenarios, consider using multi-site binding models or specialized ITC analysis software."
867
+ )
868
+
869
+ return "\n".join(log)
870
+
871
+
872
+ def analyze_protein_conservation(protein_sequences, output_dir="./"):
873
+ """Perform multiple sequence alignment and phylogenetic analysis to identify conserved protein regions.
874
+
875
+ Parameters
876
+ ----------
877
+ protein_sequences : list of str
878
+ List of protein sequences in FASTA format from multiple organisms.
879
+ output_dir : str, optional
880
+ Directory to save output files (default: "./output")
881
+
882
+ Returns
883
+ -------
884
+ str
885
+ Research log summarizing the analysis steps and results, including filenames of saved outputs.
886
+
887
+ """
888
+ import os
889
+
890
+ from Bio import AlignIO, Phylo, SeqIO
891
+ from Bio.Align.Applications import MuscleCommandline
892
+ from Bio.Phylo.TreeConstruction import DistanceCalculator, DistanceTreeConstructor
893
+
894
+ # Create a research log
895
+ log = []
896
+ log.append("# Protein Sequence Alignment and Conservation Analysis")
897
+
898
+ # Ensure output directory exists
899
+ if not os.path.exists(output_dir):
900
+ os.makedirs(output_dir)
901
+
902
+ # Step 1: Save input sequences to a temporary file
903
+ log.append("\n## Step 1: Preparing Input Sequences")
904
+ input_file = os.path.join(output_dir, "input.fasta")
905
+
906
+ # Check if input is already in FASTA format or needs conversion
907
+ if isinstance(protein_sequences, list):
908
+ if all(">" in seq for seq in protein_sequences):
909
+ # Already in FASTA format
910
+ with open(input_file, "w") as f:
911
+ f.write("\n".join(protein_sequences))
912
+ else:
913
+ # Convert to FASTA format
914
+ with open(input_file, "w") as f:
915
+ for i, seq in enumerate(protein_sequences):
916
+ f.write(f">Sequence_{i + 1}\n{seq}\n")
917
+ else:
918
+ # Assume it's a single string in FASTA format
919
+ with open(input_file, "w") as f:
920
+ f.write(protein_sequences)
921
+
922
+ log.append(f"Input sequences saved to {input_file}")
923
+ log.append(f"Number of sequences: {len(list(SeqIO.parse(input_file, 'fasta')))}")
924
+
925
+ # Step 2: Perform multiple sequence alignment using MUSCLE
926
+ log.append("\n## Step 2: Multiple Sequence Alignment")
927
+ aligned_file = os.path.join(output_dir, "aligned.fasta")
928
+
929
+ try:
930
+ # Run MUSCLE for multiple sequence alignment
931
+ muscle_cline = MuscleCommandline(input=input_file, out=aligned_file)
932
+ stdout, stderr = muscle_cline()
933
+ log.append("Multiple sequence alignment completed using MUSCLE")
934
+ log.append(f"Alignment saved to {aligned_file}")
935
+
936
+ # Load the alignment
937
+ alignment = AlignIO.read(aligned_file, "fasta")
938
+ log.append(f"Alignment length: {alignment.get_alignment_length()} positions")
939
+
940
+ except Exception as e:
941
+ log.append(f"MUSCLE alignment failed: {str(e)}")
942
+ log.append("Attempting to use Biopython's built-in alignment methods...")
943
+
944
+ # Fallback to a simpler approach without pairwise2
945
+ from Bio.Align import MultipleSeqAlignment
946
+ from Bio.Seq import Seq
947
+ from Bio.SeqRecord import SeqRecord
948
+
949
+ sequences = list(SeqIO.parse(input_file, "fasta"))
950
+
951
+ # Simple approach: just pad sequences to the same length
952
+ max_length = max(len(seq.seq) for seq in sequences)
953
+ alignments = []
954
+
955
+ for seq in sequences:
956
+ # Pad sequence to max length
957
+ padded_seq = str(seq.seq).ljust(max_length, "-")
958
+ new_seq = SeqRecord(Seq(padded_seq), id=seq.id, description=seq.description)
959
+ alignments.append(new_seq)
960
+
961
+ # Create and save the alignment
962
+ msa = MultipleSeqAlignment(alignments)
963
+ AlignIO.write(msa, aligned_file, "fasta")
964
+ alignment = msa
965
+ log.append("Simple padding alignment completed as fallback method")
966
+ log.append(f"Alignment saved to {aligned_file}")
967
+
968
+ # Step 3: Generate a phylogenetic tree
969
+ log.append("\n## Step 3: Phylogenetic Analysis")
970
+ tree_file = os.path.join(output_dir, "tree.newick")
971
+
972
+ # Calculate distance matrix
973
+ calculator = DistanceCalculator("identity")
974
+ dm = calculator.get_distance(alignment)
975
+
976
+ # Construct the phylogenetic tree using neighbor-joining method
977
+ constructor = DistanceTreeConstructor()
978
+ tree = constructor.nj(dm)
979
+
980
+ # Save the tree
981
+ Phylo.write(tree, tree_file, "newick")
982
+ log.append("Phylogenetic tree constructed using neighbor-joining method")
983
+ log.append(f"Tree saved to {tree_file}")
984
+
985
+ # Step 4: Analyze conserved regions
986
+ log.append("\n## Step 4: Conservation Analysis")
987
+ conservation_file = os.path.join(output_dir, "conservation.txt")
988
+
989
+ # Simple conservation analysis
990
+ alignment_length = alignment.get_alignment_length()
991
+ conserved_positions = []
992
+
993
+ with open(conservation_file, "w") as f:
994
+ f.write("Position\tConservation_Score\tConsensus\n")
995
+
996
+ for i in range(alignment_length):
997
+ # Get all amino acids at this position
998
+ column = alignment[:, i]
999
+ set(column)
1000
+
1001
+ # Calculate a simple conservation score (percentage of most common AA)
1002
+ most_common_aa = max(column, key=column.count)
1003
+ conservation_score = column.count(most_common_aa) / len(column)
1004
+
1005
+ f.write(f"{i + 1}\t{conservation_score:.2f}\t{most_common_aa}\n")
1006
+
1007
+ # Consider positions with >80% conservation as conserved
1008
+ if conservation_score > 0.8:
1009
+ conserved_positions.append(i + 1)
1010
+
1011
+ log.append(f"Conservation analysis completed and saved to {conservation_file}")
1012
+ log.append(f"Identified {len(conserved_positions)} highly conserved positions (>80% conservation)")
1013
+
1014
+ if conserved_positions:
1015
+ log.append(f"Conserved positions: {', '.join(map(str, conserved_positions[:10]))}")
1016
+ if len(conserved_positions) > 10:
1017
+ log.append(f"... and {len(conserved_positions) - 10} more")
1018
+
1019
+ # Final summary
1020
+ log.append("\n## Summary")
1021
+ log.append("The analysis successfully completed with the following outputs:")
1022
+ log.append(f"1. Multiple sequence alignment: {aligned_file}")
1023
+ log.append(f"2. Phylogenetic tree: {tree_file}")
1024
+ log.append(f"3. Conservation analysis: {conservation_file}")
1025
+
1026
+ return "\n".join(log)
Biomni/biomni/tool/bioimaging.py ADDED
@@ -0,0 +1,1394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import zipfile
4
+
5
+ import matplotlib
6
+ import requests
7
+
8
+ matplotlib.use("Agg") # Use non-interactive backend
9
+ import nibabel as nib
10
+ import numpy as np
11
+ import SimpleITK as sitk
12
+ import torch
13
+ import torch.serialization
14
+ from nnunet.inference.predict import predict_from_folder
15
+
16
+ # Apply safe globals for torch serialization
17
+ torch.serialization.add_safe_globals([tuple, list, dict, set, int, float, str, bytes, bytearray])
18
+ torch.serialization.add_safe_globals([complex, slice, range])
19
+ torch.serialization.add_safe_globals([np.core.multiarray.scalar])
20
+
21
+ # Configure logging
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ # ============================================================================
26
+ # SEGMENTATION CLASS
27
+ # ============================================================================
28
+
29
+
30
+ class SegmentationTool:
31
+ """
32
+ A comprehensive tool for medical image segmentation using nnUNet.
33
+ Handles BRATS dataset processing, modality splitting, and segmentation visualization.
34
+ """
35
+
36
+ def __init__(self):
37
+ """Initialize the SegmentationTool."""
38
+ self.supported_formats = [".nii", ".nii.gz"]
39
+ logger.info("SegmentationTool initialized")
40
+
41
+ def split_modalities(self, input_file, output_dir, case_name="BRAT"):
42
+ """
43
+ Split a 4D NIfTI file into separate modality files for nnUNet
44
+ Args:
45
+ input_file: Path to the 4D NIfTI file
46
+ output_dir: Directory to save the split files
47
+ case_name: Base name for the case (default: BRAT)
48
+ Returns:
49
+ output_dir: Path to directory containing split modality files
50
+ """
51
+ os.makedirs(output_dir, exist_ok=True)
52
+
53
+ # Load the 4D image
54
+ print(f"Loading {input_file}...")
55
+ img = nib.load(input_file)
56
+ data = img.get_fdata()
57
+
58
+ print(f"Image shape: {data.shape}")
59
+ print("Expected shape: (X, Y, Z, 4) for 4 modalities")
60
+
61
+ if len(data.shape) != 4:
62
+ raise ValueError(f"Expected 4D image, got {len(data.shape)}D")
63
+
64
+ if data.shape[3] != 4:
65
+ raise ValueError(f"Expected 4 modalities, got {data.shape[3]}")
66
+
67
+ # Split into separate files
68
+ modalities = ["FLAIR", "T1w", "t1gd", "T2w"]
69
+
70
+ for i, modality in enumerate(modalities):
71
+ # Extract the modality data
72
+ modality_data = data[:, :, :, i]
73
+
74
+ # Create a new NIfTI image with the same header but 3D data
75
+ modality_img = nib.Nifti1Image(modality_data, img.affine, img.header)
76
+
77
+ # Save with the expected naming convention
78
+ output_file = os.path.join(output_dir, f"{case_name}_{i:04d}.nii.gz")
79
+ nib.save(modality_img, output_file)
80
+
81
+ print(f"Saved {modality} modality to {output_file}")
82
+ print(f" Shape: {modality_data.shape}, Data type: {modality_data.dtype}")
83
+
84
+ print(f"\nAll modalities saved to {output_dir}")
85
+ return output_dir
86
+
87
+ def prepare_input_for_nnunet(self, input_path, output_dir, case_name="BRAT"):
88
+ """
89
+ Prepare input data for nnUNet by handling both 4D and pre-split modality files
90
+ Args:
91
+ input_path: Path to input file or directory
92
+ output_dir: Directory to save prepared files
93
+ case_name: Base name for the case (default: BRAT)
94
+ Returns:
95
+ prepared_dir: Path to directory with nnUNet-ready files
96
+ """
97
+ os.makedirs(output_dir, exist_ok=True)
98
+
99
+ if os.path.isfile(input_path):
100
+ # Single file - check if it's 4D
101
+ if input_path.endswith((".nii", ".nii.gz")):
102
+ try:
103
+ img = nib.load(input_path)
104
+ if len(img.shape) == 4 and img.shape[3] == 4:
105
+ print("4D NIfTI file detected, splitting modalities...")
106
+ return self.split_modalities(input_path, output_dir, case_name)
107
+ else:
108
+ print("Single 3D file detected, copying to output directory...")
109
+ # Copy single file with proper naming
110
+ output_file = os.path.join(output_dir, f"{case_name}_0000.nii.gz")
111
+ import shutil
112
+
113
+ shutil.copy2(input_path, output_file)
114
+ return output_dir
115
+ except Exception as e:
116
+ print(f"Error reading file {input_path}: {e}")
117
+ raise
118
+ elif os.path.isdir(input_path):
119
+ # Directory - check if it already has split modalities
120
+ files = [f for f in os.listdir(input_path) if f.endswith((".nii", ".nii.gz"))]
121
+
122
+ if any(f.endswith("_0000.nii.gz") for f in files):
123
+ print("Directory already contains split modality files, using as-is...")
124
+ # Copy existing files to output directory
125
+ for f in files:
126
+ if f.endswith((".nii", ".nii.gz")):
127
+ import shutil
128
+
129
+ shutil.copy2(os.path.join(input_path, f), os.path.join(output_dir, f))
130
+ return output_dir
131
+ else:
132
+ # Check if there's a 4D file to split
133
+ for f in files:
134
+ if f.endswith((".nii", ".nii.gz")):
135
+ try:
136
+ img = nib.load(os.path.join(input_path, f))
137
+ if len(img.shape) == 4 and img.shape[3] == 4:
138
+ print(f"4D NIfTI file {f} detected, splitting modalities...")
139
+ return self.split_modalities(os.path.join(input_path, f), output_dir, case_name)
140
+ except Exception as e:
141
+ logging.debug("Skipping file %s during 4D check: %s", f, e)
142
+ continue
143
+
144
+ print("No 4D files found, copying existing files...")
145
+ # Copy existing files to output directory
146
+ for f in files:
147
+ if f.endswith((".nii", ".nii.gz")):
148
+ import shutil
149
+
150
+ shutil.copy2(os.path.join(input_path, f), os.path.join(output_dir, f))
151
+ return output_dir
152
+
153
+ raise ValueError(f"Input path {input_path} is neither a valid file nor directory")
154
+
155
+ def setup_nnunet_environment(self, results_folder=None, raw_data_base=None, preprocessed=None):
156
+ """
157
+ Setup nnU-Net environment variables according to official documentation
158
+ Args:
159
+ results_folder: Path to nnUNet results folder (default: ~/nnUNet_results)
160
+ raw_data_base: Path to raw data base (default: ~/nnUNet_raw_data_base)
161
+ preprocessed: Path to preprocessed data (default: ~/nnUNet_preprocessed)
162
+ """
163
+ # Set nnUNet environment variables as per official documentation
164
+ if results_folder:
165
+ os.environ["nnUNet_RESULTS_FOLDER"] = os.path.expanduser(results_folder)
166
+ elif "nnUNet_RESULTS_FOLDER" not in os.environ:
167
+ os.environ["nnUNet_RESULTS_FOLDER"] = os.path.expanduser("~/nnUNet_results")
168
+
169
+ if raw_data_base:
170
+ os.environ["nnUNet_raw_data_base"] = os.path.expanduser(raw_data_base)
171
+ elif "nnUNet_raw_data_base" not in os.environ:
172
+ os.environ["nnUNet_raw_data_base"] = os.path.expanduser("~/nnUNet_raw_data_base")
173
+
174
+ if preprocessed:
175
+ os.environ["nnUNet_preprocessed"] = os.path.expanduser(preprocessed)
176
+ elif "nnUNet_preprocessed" not in os.environ:
177
+ os.environ["nnUNet_preprocessed"] = os.path.expanduser("~/nnUNet_preprocessed")
178
+
179
+ # Create directories if they don't exist
180
+ for path in [
181
+ os.environ["nnUNet_RESULTS_FOLDER"],
182
+ os.environ["nnUNet_raw_data_base"],
183
+ os.environ["nnUNet_preprocessed"],
184
+ ]:
185
+ os.makedirs(path, exist_ok=True)
186
+
187
+ print("nnU-Net environment variables set:")
188
+ print(f" nnUNet_RESULTS_FOLDER: {os.environ['nnUNet_RESULTS_FOLDER']}")
189
+ print(f" nnUNet_raw_data_base: {os.environ['nnUNet_raw_data_base']}")
190
+ print(f" nnUNet_preprocessed: {os.environ['nnUNet_preprocessed']}")
191
+
192
+ def _download_model_with_browser_headers(self, url, output_path):
193
+ """
194
+ Download model with browser-like headers to bypass Zenodo's anti-bot protection
195
+ """
196
+ headers = {
197
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
198
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
199
+ "Accept-Language": "en-US,en;q=0.5",
200
+ "Accept-Encoding": "gzip, deflate",
201
+ "Connection": "keep-alive",
202
+ "Upgrade-Insecure-Requests": "1",
203
+ }
204
+
205
+ logger.info(f"Downloading model from: {url}")
206
+
207
+ try:
208
+ response = requests.get(url, headers=headers, stream=True, timeout=300)
209
+ response.raise_for_status()
210
+
211
+ with open(output_path, "wb") as f:
212
+ for chunk in response.iter_content(chunk_size=8192):
213
+ if chunk:
214
+ f.write(chunk)
215
+
216
+ logger.info(f"Download completed: {output_path}")
217
+ return True
218
+
219
+ except Exception as e:
220
+ logger.error(f"Download failed: {e}")
221
+ if os.path.exists(output_path):
222
+ os.remove(output_path)
223
+ return False
224
+
225
+ def _download_and_extract_model(self, task_id, model_type="3d_fullres"):
226
+ """
227
+ Download and extract nnUNet model with browser-like headers - extracts directly to nnUNet directory
228
+ """
229
+ results_folder = os.environ.get("nnUNet_RESULTS_FOLDER", "~/nnUNet_results")
230
+ results_folder = os.path.expanduser(results_folder)
231
+
232
+ # Create the nnUNet directory
233
+ nnunet_dir = os.path.join(results_folder, "nnUNet")
234
+ os.makedirs(nnunet_dir, exist_ok=True)
235
+
236
+ # Check if model already exists
237
+ task_dir = os.path.join(nnunet_dir, model_type, task_id)
238
+ if os.path.exists(task_dir):
239
+ # Check if it has the expected structure
240
+ plans_file = os.path.join(task_dir, "nnUNetTrainerV2__nnUNetPlansv2.1")
241
+ if os.path.exists(plans_file):
242
+ logger.info(f"Model already exists for task '{task_id}' with {model_type}")
243
+ return True
244
+
245
+ # Download URL for the task
246
+ download_url = f"https://zenodo.org/record/4003545/files/{task_id}.zip?download=1"
247
+
248
+ # Create temporary file for download
249
+ temp_zip = os.path.join(nnunet_dir, f"{task_id}_temp.zip")
250
+
251
+ # Download with browser headers
252
+ if self._download_model_with_browser_headers(download_url, temp_zip):
253
+ try:
254
+ logger.info(f"Extracting {temp_zip} to {nnunet_dir}")
255
+ with zipfile.ZipFile(temp_zip, "r") as zip_ref:
256
+ zip_ref.extractall(nnunet_dir)
257
+
258
+ # Remove temporary zip file
259
+ os.remove(temp_zip)
260
+
261
+ # Verify extraction worked
262
+ if os.path.exists(task_dir):
263
+ logger.info(f"Model successfully downloaded and extracted for task '{task_id}'")
264
+ logger.info(f"Model directory: {task_dir}")
265
+ return True
266
+ else:
267
+ logger.error(f"Task directory not found after extraction: {task_dir}")
268
+ return False
269
+
270
+ except Exception as e:
271
+ logger.error(f"Extraction failed: {e}")
272
+ if os.path.exists(temp_zip):
273
+ os.remove(temp_zip)
274
+ return False
275
+ else:
276
+ return False
277
+
278
+ def segment_with_nn_unet(
279
+ self,
280
+ image_path,
281
+ output_dir,
282
+ task_id,
283
+ model_type="3d_fullres",
284
+ folds=None,
285
+ use_tta=False,
286
+ num_threads=1,
287
+ mixed_precision=True,
288
+ verbose=True,
289
+ auto_prepare_input=True,
290
+ results_folder=None,
291
+ auto_download=True,
292
+ ):
293
+ """
294
+ Segment images using nnUNet with proper environment setup and automatic model downloading
295
+ Args:
296
+ image_path: Path to input image file or directory
297
+ output_dir: Directory to save segmentation results
298
+ task_id: Task identifier (e.g., 'Task001_BrainTumour')
299
+ model_type: Model type (default: '3d_fullres')
300
+ folds: Model folds to use (default: [0, 1, 2, 3, 4])
301
+ use_tta: Use test time augmentation (default: False)
302
+ num_threads: Number of threads for preprocessing (default: 1)
303
+ mixed_precision: Use mixed precision (default: True)
304
+ verbose: Verbose logging (default: True)
305
+ auto_prepare_input: Automatically prepare input for nnUNet (default: True)
306
+ results_folder: Path to nnUNet results folder (default: None, will use environment variable or default)
307
+ auto_download: Automatically download missing models (default: True)
308
+ """
309
+ if folds is None:
310
+ folds = [0, 1, 2, 3, 4]
311
+ os.makedirs(output_dir, exist_ok=True)
312
+ logging.basicConfig(level=logging.INFO if verbose else logging.WARNING)
313
+
314
+ # Setup nnUNet environment first
315
+ self.setup_nnunet_environment(results_folder=results_folder)
316
+
317
+ # Prepare input data if requested
318
+ if auto_prepare_input:
319
+ temp_input_dir = os.path.join(output_dir, "temp_input")
320
+ prepared_input_dir = self.prepare_input_for_nnunet(image_path, temp_input_dir)
321
+ image_path = prepared_input_dir
322
+ logging.info(f"Input prepared for nnUNet: {image_path}")
323
+
324
+ logging.info("Verifying NIfTI input files...")
325
+
326
+ def verify_nifti_input(image_path):
327
+ if os.path.isfile(image_path):
328
+ nib.load(image_path)
329
+ else:
330
+ for file in os.listdir(image_path):
331
+ if file.endswith(".nii") or file.endswith(".nii.gz"):
332
+ nib.load(os.path.join(image_path, file))
333
+
334
+ verify_nifti_input(image_path)
335
+
336
+ # Get or set up the results folder
337
+ results_folder = os.environ.get("nnUNet_RESULTS_FOLDER")
338
+ if not results_folder:
339
+ # Try common locations
340
+ common_paths = [
341
+ "./models/nnUNet",
342
+ "~/nnUNet_results",
343
+ "~/biomni_models/nnUNet",
344
+ ]
345
+
346
+ for base_path in common_paths:
347
+ expanded_path = os.path.expanduser(base_path)
348
+ if os.path.exists(expanded_path):
349
+ results_folder = expanded_path
350
+ break
351
+
352
+ # If still no folder found, create one
353
+ if not results_folder:
354
+ results_folder = os.path.expanduser("~/nnUNet_results")
355
+ os.makedirs(results_folder, exist_ok=True)
356
+ logging.info(f"Created new results folder: {results_folder}")
357
+
358
+ os.environ["RESULTS_FOLDER"] = results_folder
359
+ os.environ["nnUNet_RESULTS_FOLDER"] = results_folder
360
+ logging.info(f"Set RESULTS_FOLDER environment variable to: {results_folder}")
361
+
362
+ # Construct the expected model path - using the correct structure
363
+ model_folder = os.path.join(results_folder, "nnUNet", model_type, task_id, "nnUNetTrainerV2__nnUNetPlansv2.1")
364
+
365
+ logging.info(f"Looking for model at: {model_folder}")
366
+
367
+ # Check if model files actually exist (not just the directory)
368
+ def check_model_files_exist(model_folder, folds):
369
+ """Check if actual model weight files exist for the specified folds"""
370
+ if not os.path.exists(model_folder):
371
+ return False
372
+
373
+ # Check for plans.pkl and other required files
374
+ required_files = ["plans.pkl"]
375
+ for req_file in required_files:
376
+ if not os.path.exists(os.path.join(model_folder, req_file)):
377
+ return False
378
+
379
+ # Check if at least one fold has model files
380
+ for fold in folds:
381
+ fold_dir = os.path.join(model_folder, f"fold_{fold}")
382
+ if os.path.exists(fold_dir):
383
+ # Look for model files (.model, .model.pkl, etc.)
384
+ model_files = [f for f in os.listdir(fold_dir) if f.endswith((".model", ".model.pkl"))]
385
+ if model_files:
386
+ return True
387
+ return False
388
+
389
+ # Check if model exists, download if not
390
+ if not check_model_files_exist(model_folder, folds):
391
+ if auto_download:
392
+ logging.info(f"Model weights for {task_id} not found. Downloading...")
393
+ # Ensure the results folder structure exists
394
+ os.makedirs(os.path.dirname(model_folder), exist_ok=True)
395
+
396
+ # Download the model using our custom download function
397
+ if self._download_and_extract_model(task_id, model_type):
398
+ logging.info(f"Downloaded pretrained model for {task_id} successfully.")
399
+ else:
400
+ raise RuntimeError(f"Failed to download model for {task_id}")
401
+
402
+ # Verify the download worked
403
+ if not check_model_files_exist(model_folder, folds):
404
+ raise RuntimeError(
405
+ f"Model download completed but files not found at expected location: {model_folder}"
406
+ )
407
+ else:
408
+ # Ask user for permission to download
409
+ user_input = (
410
+ input(f"Model weights for {task_id} not found. Do you want to download them? (y/n): ")
411
+ .strip()
412
+ .lower()
413
+ )
414
+ if user_input == "y":
415
+ # Download the model using our custom download function
416
+ if self._download_and_extract_model(task_id, model_type):
417
+ logging.info(f"Downloaded pretrained model for {task_id} successfully.")
418
+ else:
419
+ raise RuntimeError(f"Failed to download model for {task_id}")
420
+ else:
421
+ raise RuntimeError("Model weights not found and download declined by user.")
422
+
423
+ # Double-check that we now have the model
424
+ if not check_model_files_exist(model_folder, folds):
425
+ raise RuntimeError(f"Model files still not found at {model_folder} after download attempt")
426
+
427
+ logging.info(f"Using model: {model_folder}")
428
+
429
+ # Patch torch.load for compatibility
430
+ original_torch_load = torch.load
431
+
432
+ def patched_torch_load(*args, **kwargs):
433
+ kwargs["weights_only"] = False
434
+ return original_torch_load(*args, **kwargs)
435
+
436
+ # Run the segmentation
437
+ torch.load = patched_torch_load
438
+ try:
439
+ predict_from_folder(
440
+ model=model_folder,
441
+ input_folder=image_path,
442
+ output_folder=output_dir,
443
+ folds=folds,
444
+ save_npz=False,
445
+ num_threads_preprocessing=num_threads,
446
+ num_threads_nifti_save=num_threads,
447
+ mixed_precision=mixed_precision,
448
+ lowres_segmentations=None,
449
+ part_id=0,
450
+ num_parts=1,
451
+ tta=use_tta,
452
+ )
453
+ finally:
454
+ # Restore original torch.load behavior
455
+ torch.load = original_torch_load
456
+
457
+ # Clean up temporary input directory if it was created
458
+ if auto_prepare_input and os.path.exists(temp_input_dir):
459
+ import shutil
460
+
461
+ shutil.rmtree(temp_input_dir)
462
+ logging.info("Cleaned up temporary input directory")
463
+
464
+ logging.info(f"Segmentation outputs stored at: {output_dir}")
465
+ return output_dir
466
+
467
+ def create_segmentation_visualization(self, original_mri, segmentation, output_dir="./visualization_output"):
468
+ """
469
+ Create and save visualization of segmentation results using nilearn
470
+ Args:
471
+ original_mri: Path to original MRI file
472
+ segmentation: Path to segmentation file
473
+ output_dir: Directory to save visualization images
474
+ Returns:
475
+ list: List of saved image file paths
476
+ """
477
+ try:
478
+ # Import nilearn here to avoid dependency issues
479
+ from nilearn import plotting
480
+
481
+ # Create output directory
482
+ os.makedirs(output_dir, exist_ok=True)
483
+
484
+ # Check if files exist
485
+ if not os.path.exists(original_mri):
486
+ raise FileNotFoundError(f"Original MRI file not found: {original_mri}")
487
+
488
+ if not os.path.exists(segmentation):
489
+ raise FileNotFoundError(f"Segmentation file not found: {segmentation}")
490
+
491
+ print("✅ Files found, creating visualizations...")
492
+ saved_files = []
493
+
494
+ # Create and save the main overlay plot
495
+ display = plotting.plot_roi(
496
+ segmentation,
497
+ bg_img=original_mri,
498
+ cmap="Set1",
499
+ alpha=0.6,
500
+ title="Segmentation Overlay",
501
+ )
502
+
503
+ # Save the main overlay plot
504
+ output_file = os.path.join(output_dir, "segmentation_overlay.png")
505
+ display.savefig(output_file, dpi=150, bbox_inches="tight")
506
+ saved_files.append(output_file)
507
+ print(f"✅ Main overlay saved to: {output_file}")
508
+ display.close()
509
+
510
+ # Create additional views and save them
511
+ # Axial view
512
+ display_axial = plotting.plot_roi(
513
+ segmentation,
514
+ bg_img=original_mri,
515
+ cmap="Set1",
516
+ alpha=0.6,
517
+ title="Segmentation Overlay - Axial View",
518
+ display_mode="z",
519
+ )
520
+ axial_file = os.path.join(output_dir, "segmentation_axial.png")
521
+ display_axial.savefig(axial_file, dpi=150, bbox_inches="tight")
522
+ saved_files.append(axial_file)
523
+ print(f"✅ Axial view saved to: {axial_file}")
524
+ display_axial.close()
525
+
526
+ # Sagittal view
527
+ display_sagittal = plotting.plot_roi(
528
+ segmentation,
529
+ bg_img=original_mri,
530
+ cmap="Set1",
531
+ alpha=0.6,
532
+ title="Segmentation Overlay - Sagittal View",
533
+ display_mode="x",
534
+ )
535
+ sagittal_file = os.path.join(output_dir, "segmentation_sagittal.png")
536
+ display_sagittal.savefig(sagittal_file, dpi=150, bbox_inches="tight")
537
+ saved_files.append(sagittal_file)
538
+ print(f"✅ Sagittal view saved to: {sagittal_file}")
539
+ display_sagittal.close()
540
+
541
+ # Coronal view
542
+ display_coronal = plotting.plot_roi(
543
+ segmentation,
544
+ bg_img=original_mri,
545
+ cmap="Set1",
546
+ alpha=0.6,
547
+ title="Segmentation Overlay - Coronal View",
548
+ display_mode="y",
549
+ )
550
+ coronal_file = os.path.join(output_dir, "segmentation_coronal.png")
551
+ display_coronal.savefig(coronal_file, dpi=150, bbox_inches="tight")
552
+ saved_files.append(coronal_file)
553
+ print(f"✅ Coronal view saved to: {coronal_file}")
554
+ display_coronal.close()
555
+
556
+ print(f"\n✅ All visualizations saved to: {output_dir}")
557
+ print("Files created:")
558
+ for file_path in saved_files:
559
+ print(f" - {os.path.basename(file_path)}")
560
+
561
+ return saved_files
562
+
563
+ except ImportError:
564
+ print(" nilearn not available. Install with: pip install nilearn")
565
+ return []
566
+ except Exception as e:
567
+ print(f" Visualization failed: {e}")
568
+ import traceback
569
+
570
+ traceback.print_exc()
571
+ return []
572
+
573
+
574
+ # ============================================================================
575
+ # IMAGE REGISTRATION CLASS
576
+ # ============================================================================
577
+
578
+
579
+ class ImageRegistrationTool:
580
+ """
581
+ A comprehensive tool for medical image registration using SimpleITK.
582
+ Supports rigid, affine, and deformable registration with preprocessing and visualization.
583
+ """
584
+
585
+ def __init__(self):
586
+ """Initialize the ImageRegistrationTool."""
587
+ self.supported_formats = [".nii", ".nii.gz", ".nrrd", ".mha", ".mhd"]
588
+ logger.info("ImageRegistrationTool initialized")
589
+
590
+ def load_image(self, image_path: str) -> sitk.Image:
591
+ """
592
+ Load a medical image using SimpleITK.
593
+
594
+ Args:
595
+ image_path: Path to the image file
596
+
597
+ Returns:
598
+ SimpleITK Image object
599
+ """
600
+ if not os.path.exists(image_path):
601
+ raise FileNotFoundError(f"Image file not found: {image_path}")
602
+
603
+ logger.info(f"Loading image: {image_path}")
604
+ try:
605
+ image = sitk.ReadImage(image_path)
606
+ logger.info(f"Successfully loaded image with size: {image.GetSize()}")
607
+ return image
608
+ except Exception as e:
609
+ logger.error(f"Failed to load image {image_path}: {e}")
610
+ raise
611
+
612
+ def save_image(self, image: sitk.Image, output_path: str) -> None:
613
+ """
614
+ Save a SimpleITK image to file.
615
+
616
+ Args:
617
+ image: SimpleITK Image object
618
+ output_path: Path to save the image (must include filename and extension)
619
+ """
620
+ # Validate output path
621
+ if os.path.isdir(output_path):
622
+ raise ValueError(
623
+ f"Output path '{output_path}' is a directory. Please provide a file path with extension (e.g., '.nii.gz', '.png', '.jpg')"
624
+ )
625
+
626
+ if not os.path.splitext(output_path)[1]:
627
+ raise ValueError(
628
+ f"Output path '{output_path}' has no file extension. Please add an extension (e.g., '.nii.gz', '.png', '.jpg')"
629
+ )
630
+
631
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
632
+ logger.info(f"Saving image to: {output_path}")
633
+ try:
634
+ sitk.WriteImage(image, output_path)
635
+ logger.info("Image saved successfully")
636
+ except Exception as e:
637
+ logger.error(f"Failed to save image to {output_path}: {e}")
638
+ raise
639
+
640
+ def preprocess_image(self, image: sitk.Image, denoise: bool = True, normalize: bool = True) -> sitk.Image:
641
+ """
642
+ Preprocess an image with denoising and normalization.
643
+
644
+ Args:
645
+ image: Input SimpleITK image
646
+ denoise: Whether to apply denoising
647
+ normalize: Whether to apply normalization
648
+
649
+ Returns:
650
+ Preprocessed SimpleITK image
651
+ """
652
+ logger.info("Preprocessing image...")
653
+ processed_image = sitk.Image(image)
654
+
655
+ if denoise:
656
+ # Apply Gaussian smoothing for denoising
657
+ processed_image = sitk.SmoothingRecursiveGaussian(processed_image, sigma=1.0)
658
+ logger.info("Applied denoising")
659
+
660
+ if normalize:
661
+ # Normalize to [0, 1] range
662
+ min_max_filter = sitk.MinimumMaximumImageFilter()
663
+ min_max_filter.Execute(processed_image)
664
+ min_val = min_max_filter.GetMinimum()
665
+ max_val = min_max_filter.GetMaximum()
666
+
667
+ if max_val > min_val:
668
+ processed_image = sitk.IntensityWindowing(
669
+ processed_image, windowMinimum=min_val, windowMaximum=max_val, outputMinimum=0.0, outputMaximum=1.0
670
+ )
671
+ logger.info("Applied normalization")
672
+
673
+ return processed_image
674
+
675
+ def create_rigid_transform(
676
+ self, fixed_image: sitk.Image, moving_image: sitk.Image, initial_transform: sitk.Transform | None = None
677
+ ) -> sitk.Transform:
678
+ """
679
+ Create a rigid transform for image registration.
680
+
681
+ Args:
682
+ fixed_image: Reference (fixed) image
683
+ moving_image: Image to be registered
684
+ initial_transform: Optional initial transform
685
+
686
+ Returns:
687
+ Rigid transform object
688
+ """
689
+ logger.info("Creating rigid transform...")
690
+
691
+ if initial_transform is None:
692
+ # Create identity transform
693
+ transform = sitk.Euler3DTransform()
694
+ else:
695
+ transform = initial_transform
696
+
697
+ return transform
698
+
699
+ def create_affine_transform(
700
+ self, fixed_image: sitk.Image, moving_image: sitk.Image, initial_transform: sitk.Transform | None = None
701
+ ) -> sitk.Transform:
702
+ """
703
+ Create an affine transform for image registration.
704
+
705
+ Args:
706
+ fixed_image: Reference (fixed) image
707
+ moving_image: Image to be registered
708
+ initial_transform: Optional initial transform
709
+
710
+ Returns:
711
+ Affine transform object
712
+ """
713
+ logger.info("Creating affine transform...")
714
+
715
+ if initial_transform is None:
716
+ # Create identity transform
717
+ transform = sitk.AffineTransform(3)
718
+ else:
719
+ transform = initial_transform
720
+
721
+ return transform
722
+
723
+ def create_deformable_transform(
724
+ self, fixed_image: sitk.Image, moving_image: sitk.Image, number_of_control_points: int = 4
725
+ ) -> sitk.Transform:
726
+ """
727
+ Create a deformable (B-spline) transform for image registration.
728
+
729
+ Args:
730
+ fixed_image: Reference (fixed) image
731
+ moving_image: Image to be registered
732
+ number_of_control_points: Number of B-spline control points per dimension
733
+
734
+ Returns:
735
+ Deformable transform object
736
+ """
737
+ logger.info("Creating deformable transform...")
738
+
739
+ # Create B-spline transform
740
+ transform = sitk.BSplineTransformInitializer(fixed_image, [number_of_control_points] * 3, order=3)
741
+
742
+ return transform
743
+
744
+ def setup_registration_method(
745
+ self,
746
+ transform: sitk.Transform,
747
+ metric: str = "mutual_information",
748
+ optimizer: str = "gradient_descent",
749
+ learning_rate: float = 0.01,
750
+ number_of_iterations: int = 100,
751
+ gradient_convergence_tolerance: float = 1e-6,
752
+ ) -> sitk.ImageRegistrationMethod:
753
+ """
754
+ Setup the registration method with specified parameters.
755
+
756
+ Args:
757
+ transform: Transform object
758
+ metric: Similarity metric name
759
+ optimizer: Optimizer name
760
+ learning_rate: Learning rate for gradient descent
761
+ number_of_iterations: Maximum number of iterations
762
+ gradient_convergence_tolerance: Convergence tolerance
763
+
764
+ Returns:
765
+ Configured registration method
766
+ """
767
+ logger.info(f"Setting up registration method: {metric} metric, {optimizer} optimizer")
768
+
769
+ registration_method = sitk.ImageRegistrationMethod()
770
+
771
+ # Set similarity metric
772
+ if metric == "mutual_information":
773
+ registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
774
+ elif metric == "mean_squares":
775
+ registration_method.SetMetricAsMeanSquares()
776
+ elif metric == "correlation":
777
+ registration_method.SetMetricAsCorrelation()
778
+ elif metric == "normalized_correlation":
779
+ registration_method.SetMetricAsNormalizedCorrelation()
780
+ else:
781
+ logger.warning(f"Unknown metric {metric}, using mutual information")
782
+ registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
783
+
784
+ # Set optimizer
785
+ if optimizer == "gradient_descent":
786
+ registration_method.SetOptimizerAsGradientDescent(
787
+ learningRate=learning_rate,
788
+ numberOfIterations=number_of_iterations,
789
+ convergenceMinimumValue=gradient_convergence_tolerance,
790
+ )
791
+ elif optimizer == "lbfgsb":
792
+ registration_method.SetOptimizerAsLBFGSB(
793
+ gradientConvergenceTolerance=gradient_convergence_tolerance, numberOfIterations=number_of_iterations
794
+ )
795
+ elif optimizer == "powell":
796
+ registration_method.SetOptimizerAsPowell(numberOfIterations=number_of_iterations, maximumLineIterations=20)
797
+ elif optimizer == "amoeba":
798
+ registration_method.SetOptimizerAsAmoeba(
799
+ numberOfIterations=number_of_iterations, parametersConvergenceTolerance=gradient_convergence_tolerance
800
+ )
801
+ else:
802
+ logger.warning(f"Unknown optimizer {optimizer}, using gradient descent")
803
+ registration_method.SetOptimizerAsGradientDescent(
804
+ learningRate=learning_rate,
805
+ numberOfIterations=number_of_iterations,
806
+ convergenceMinimumValue=gradient_convergence_tolerance,
807
+ )
808
+
809
+ # Set interpolator
810
+ registration_method.SetInterpolator(sitk.sitkLinear)
811
+
812
+ # Set initial transform
813
+ registration_method.SetInitialTransform(transform, inPlace=False)
814
+
815
+ return registration_method
816
+
817
+ def register_images(
818
+ self,
819
+ fixed_image: sitk.Image,
820
+ moving_image: sitk.Image,
821
+ transform: sitk.Transform,
822
+ registration_method: sitk.ImageRegistrationMethod,
823
+ ) -> tuple[sitk.Transform, sitk.Image]:
824
+ """
825
+ Perform image registration.
826
+
827
+ Args:
828
+ fixed_image: Reference (fixed) image
829
+ moving_image: Image to be registered
830
+ transform: Transform object
831
+ registration_method: Configured registration method
832
+
833
+ Returns:
834
+ Tuple of (final_transform, registered_image)
835
+ """
836
+ logger.info("Starting image registration...")
837
+
838
+ # Add iteration callback
839
+ def command_iteration():
840
+ logger.info(
841
+ f"Iteration: {registration_method.GetOptimizerIteration()}, "
842
+ f"Metric value: {registration_method.GetMetricValue():.6f}"
843
+ )
844
+
845
+ registration_method.AddCommand(sitk.sitkIterationEvent, command_iteration)
846
+
847
+ try:
848
+ # Execute registration
849
+ final_transform = registration_method.Execute(fixed_image, moving_image)
850
+
851
+ # Apply transform to moving image
852
+ resampler = sitk.ResampleImageFilter()
853
+ resampler.SetReferenceImage(fixed_image)
854
+ resampler.SetInterpolator(sitk.sitkLinear)
855
+ resampler.SetDefaultPixelValue(0)
856
+ resampler.SetTransform(final_transform)
857
+
858
+ registered_image = resampler.Execute(moving_image)
859
+
860
+ logger.info("Registration completed successfully")
861
+ return final_transform, registered_image
862
+
863
+ except Exception as e:
864
+ logger.error(f"Registration failed: {e}")
865
+ raise
866
+
867
+ def calculate_similarity_metrics(self, image1: sitk.Image, image2: sitk.Image) -> dict[str, float]:
868
+ """
869
+ Calculate similarity metrics between two images.
870
+
871
+ Args:
872
+ image1: First image
873
+ image2: Second image
874
+
875
+ Returns:
876
+ Dictionary of similarity metrics
877
+ """
878
+ logger.info("Calculating similarity metrics...")
879
+ metrics = {}
880
+
881
+ try:
882
+ # Convert to numpy arrays
883
+ array1 = sitk.GetArrayFromImage(image1)
884
+ array2 = sitk.GetArrayFromImage(image2)
885
+
886
+ # Flatten arrays
887
+ flat1 = array1.flatten()
888
+ flat2 = array2.flatten()
889
+
890
+ # Remove invalid values
891
+ valid_mask = np.isfinite(flat1) & np.isfinite(flat2)
892
+ flat1 = flat1[valid_mask]
893
+ flat2 = flat2[valid_mask]
894
+
895
+ if len(flat1) == 0:
896
+ logger.warning("No valid pixels for similarity calculation")
897
+ return {
898
+ "mutual_information": 0.0,
899
+ "mean_squares": 0.0,
900
+ "correlation": 0.0,
901
+ "normalized_correlation": 0.0,
902
+ }
903
+
904
+ # Mean Squared Error
905
+ mse = np.mean((flat1 - flat2) ** 2)
906
+ metrics["mean_squares"] = -mse # Negative because we want to maximize
907
+
908
+ # Pearson Correlation
909
+ if np.std(flat1) > 0 and np.std(flat2) > 0:
910
+ correlation = np.corrcoef(flat1, flat2)[0, 1]
911
+ metrics["correlation"] = correlation if not np.isnan(correlation) else 0.0
912
+ else:
913
+ metrics["correlation"] = 0.0
914
+
915
+ # Normalized Cross Correlation
916
+ if np.std(flat1) > 0 and np.std(flat2) > 0:
917
+ ncc = np.corrcoef(flat1, flat2)[0, 1]
918
+ metrics["normalized_correlation"] = ncc if not np.isnan(ncc) else 0.0
919
+ else:
920
+ metrics["normalized_correlation"] = 0.0
921
+
922
+ # Mutual Information (simplified calculation)
923
+ try:
924
+ # Create 2D histogram
925
+ hist_2d, x_edges, y_edges = np.histogram2d(flat1, flat2, bins=50)
926
+ hist_2d = hist_2d + 1e-10 # Add small value to avoid log(0)
927
+
928
+ # Normalize histogram
929
+ pxy = hist_2d / np.sum(hist_2d)
930
+ px = np.sum(pxy, axis=1)
931
+ py = np.sum(pxy, axis=0)
932
+
933
+ # Calculate mutual information
934
+ mi = 0.0
935
+ for i in range(len(px)):
936
+ for j in range(len(py)):
937
+ if pxy[i, j] > 0 and px[i] > 0 and py[j] > 0:
938
+ mi += pxy[i, j] * np.log2(pxy[i, j] / (px[i] * py[j]))
939
+
940
+ metrics["mutual_information"] = mi
941
+
942
+ except Exception as e:
943
+ logger.warning(f"Failed to calculate mutual information: {e}")
944
+ metrics["mutual_information"] = 0.0
945
+
946
+ except Exception as e:
947
+ logger.warning(f"Failed to calculate similarity metrics: {e}")
948
+ metrics = {
949
+ "mutual_information": 0.0,
950
+ "mean_squares": 0.0,
951
+ "correlation": 0.0,
952
+ "normalized_correlation": 0.0,
953
+ }
954
+
955
+ logger.info("Similarity metrics calculated")
956
+ return metrics
957
+
958
+
959
+ # ============================================================================
960
+ # CONVENIENCE FUNCTIONS FOR BIOMNI INTEGRATION
961
+ # ============================================================================
962
+
963
+
964
+ # Segmentation convenience functions
965
+ def split_modalities(input_file, output_dir, case_name="BRAT"):
966
+ """Convenience function for splitting modalities"""
967
+ tool = SegmentationTool()
968
+ return tool.split_modalities(input_file, output_dir, case_name)
969
+
970
+
971
+ def prepare_input_for_nnunet(input_path, output_dir, case_name="BRAT"):
972
+ """Convenience function for preparing nnUNet input"""
973
+ tool = SegmentationTool()
974
+ return tool.prepare_input_for_nnunet(input_path, output_dir, case_name)
975
+
976
+
977
+ def segment_with_nn_unet(
978
+ image_path,
979
+ output_dir,
980
+ task_id,
981
+ model_type="3d_fullres",
982
+ folds=None,
983
+ use_tta=False,
984
+ num_threads=1,
985
+ mixed_precision=True,
986
+ verbose=True,
987
+ auto_prepare_input=True,
988
+ results_folder=None,
989
+ ):
990
+ """Convenience function for nnUNet segmentation"""
991
+ tool = SegmentationTool()
992
+ return tool.segment_with_nn_unet(
993
+ image_path,
994
+ output_dir,
995
+ task_id,
996
+ model_type,
997
+ folds,
998
+ use_tta,
999
+ num_threads,
1000
+ mixed_precision,
1001
+ verbose,
1002
+ auto_prepare_input,
1003
+ results_folder,
1004
+ )
1005
+
1006
+
1007
+ def create_segmentation_visualization(original_mri, segmentation, output_dir="./visualization_output"):
1008
+ """Convenience function for segmentation visualization"""
1009
+ tool = SegmentationTool()
1010
+ return tool.create_segmentation_visualization(original_mri, segmentation, output_dir)
1011
+
1012
+
1013
+ # Registration convenience functions
1014
+ def preprocess_image(image_path: str, output_path: str, denoise: bool = True, normalize: bool = True) -> str:
1015
+ """
1016
+ Standalone image preprocessing function for Biomni integration.
1017
+
1018
+ Args:
1019
+ image_path: Path to input image
1020
+ output_path: Path to save preprocessed image
1021
+ denoise: Whether to apply denoising (default: True)
1022
+ normalize: Whether to apply normalization (default: True)
1023
+
1024
+ Returns:
1025
+ Path to the saved preprocessed image
1026
+ """
1027
+ tool = ImageRegistrationTool()
1028
+ image = tool.load_image(image_path)
1029
+ preprocessed_image = tool.preprocess_image(image, denoise, normalize)
1030
+ tool.save_image(preprocessed_image, output_path)
1031
+ return output_path
1032
+
1033
+
1034
+ def quick_rigid_registration(
1035
+ fixed_image_path: str,
1036
+ moving_image_path: str,
1037
+ output_dir: str,
1038
+ metric: str = "mutual_information",
1039
+ optimizer: str = "gradient_descent",
1040
+ preprocess: bool = True,
1041
+ create_visualizations: bool = True,
1042
+ learning_rate: float = 0.01,
1043
+ number_of_iterations: int = 100,
1044
+ gradient_convergence_tolerance: float = 1e-6,
1045
+ ) -> dict:
1046
+ """
1047
+ Quick rigid registration function for Biomni integration.
1048
+
1049
+ Args:
1050
+ fixed_image_path: Path to reference image
1051
+ moving_image_path: Path to image to register
1052
+ output_dir: Directory to save results
1053
+ metric: Similarity metric
1054
+ optimizer: Optimization method
1055
+ preprocess: Whether to preprocess images
1056
+ create_visualizations: Whether to create visualizations
1057
+ learning_rate: Learning rate for optimizer
1058
+ number_of_iterations: Maximum iterations
1059
+ gradient_convergence_tolerance: Convergence tolerance
1060
+
1061
+ Returns:
1062
+ Dictionary with registration results
1063
+ """
1064
+ tool = ImageRegistrationTool()
1065
+
1066
+ # Load images
1067
+ fixed_image = tool.load_image(fixed_image_path)
1068
+ moving_image = tool.load_image(moving_image_path)
1069
+
1070
+ # Preprocess if requested
1071
+ if preprocess:
1072
+ fixed_image = tool.preprocess_image(fixed_image)
1073
+ moving_image = tool.preprocess_image(moving_image)
1074
+
1075
+ # Create transform and registration method
1076
+ transform = tool.create_rigid_transform(fixed_image, moving_image)
1077
+ registration_method = tool.setup_registration_method(
1078
+ transform, metric, optimizer, learning_rate, number_of_iterations, gradient_convergence_tolerance
1079
+ )
1080
+
1081
+ # Perform registration
1082
+ final_transform, registered_image = tool.register_images(fixed_image, moving_image, transform, registration_method)
1083
+
1084
+ # Save results
1085
+ os.makedirs(output_dir, exist_ok=True)
1086
+
1087
+ # Save registered image
1088
+ registered_path = os.path.join(output_dir, "rigid_registered.nii.gz")
1089
+ tool.save_image(registered_image, registered_path)
1090
+
1091
+ # Save transform
1092
+ transform_path = os.path.join(output_dir, "rigid_transform.tfm")
1093
+ sitk.WriteTransform(final_transform, transform_path)
1094
+
1095
+ # Calculate metrics
1096
+ metrics_before = tool.calculate_similarity_metrics(fixed_image, moving_image)
1097
+ metrics_after = tool.calculate_similarity_metrics(fixed_image, registered_image)
1098
+
1099
+ results = {
1100
+ "registered_image_path": registered_path,
1101
+ "transform_path": transform_path,
1102
+ "metrics_before": metrics_before,
1103
+ "metrics_after": metrics_after,
1104
+ "registration_type": "rigid",
1105
+ }
1106
+
1107
+ return results
1108
+
1109
+
1110
+ def quick_affine_registration(
1111
+ fixed_image_path: str,
1112
+ moving_image_path: str,
1113
+ output_dir: str,
1114
+ metric: str = "mutual_information",
1115
+ optimizer: str = "gradient_descent",
1116
+ preprocess: bool = True,
1117
+ create_visualizations: bool = True,
1118
+ learning_rate: float = 0.01,
1119
+ number_of_iterations: int = 100,
1120
+ gradient_convergence_tolerance: float = 1e-6,
1121
+ ) -> dict:
1122
+ """
1123
+ Quick affine registration function for Biomni integration.
1124
+
1125
+ Args:
1126
+ fixed_image_path: Path to reference image
1127
+ moving_image_path: Path to image to register
1128
+ output_dir: Directory to save results
1129
+ metric: Similarity metric
1130
+ optimizer: Optimization method
1131
+ preprocess: Whether to preprocess images
1132
+ create_visualizations: Whether to create visualizations
1133
+ learning_rate: Learning rate for optimizer
1134
+ number_of_iterations: Maximum iterations
1135
+ gradient_convergence_tolerance: Convergence tolerance
1136
+
1137
+ Returns:
1138
+ Dictionary with registration results
1139
+ """
1140
+ tool = ImageRegistrationTool()
1141
+
1142
+ # Load images
1143
+ fixed_image = tool.load_image(fixed_image_path)
1144
+ moving_image = tool.load_image(moving_image_path)
1145
+
1146
+ # Preprocess if requested
1147
+ if preprocess:
1148
+ fixed_image = tool.preprocess_image(fixed_image)
1149
+ moving_image = tool.preprocess_image(moving_image)
1150
+
1151
+ # Create transform and registration method
1152
+ transform = tool.create_affine_transform(fixed_image, moving_image)
1153
+ registration_method = tool.setup_registration_method(
1154
+ transform, metric, optimizer, learning_rate, number_of_iterations, gradient_convergence_tolerance
1155
+ )
1156
+
1157
+ # Perform registration
1158
+ final_transform, registered_image = tool.register_images(fixed_image, moving_image, transform, registration_method)
1159
+
1160
+ # Save results
1161
+ os.makedirs(output_dir, exist_ok=True)
1162
+
1163
+ # Save registered image
1164
+ registered_path = os.path.join(output_dir, "affine_registered.nii.gz")
1165
+ tool.save_image(registered_image, registered_path)
1166
+
1167
+ # Save transform
1168
+ transform_path = os.path.join(output_dir, "affine_transform.tfm")
1169
+ sitk.WriteTransform(final_transform, transform_path)
1170
+
1171
+ # Calculate metrics
1172
+ metrics_before = tool.calculate_similarity_metrics(fixed_image, moving_image)
1173
+ metrics_after = tool.calculate_similarity_metrics(fixed_image, registered_image)
1174
+
1175
+ results = {
1176
+ "registered_image_path": registered_path,
1177
+ "transform_path": transform_path,
1178
+ "metrics_before": metrics_before,
1179
+ "metrics_after": metrics_after,
1180
+ "registration_type": "affine",
1181
+ }
1182
+
1183
+ return results
1184
+
1185
+
1186
+ def quick_deformable_registration(
1187
+ fixed_image_path: str,
1188
+ moving_image_path: str,
1189
+ output_dir: str,
1190
+ metric: str = "mutual_information",
1191
+ optimizer: str = "gradient_descent",
1192
+ preprocess: bool = True,
1193
+ create_visualizations: bool = True,
1194
+ learning_rate: float = 0.01,
1195
+ number_of_iterations: int = 100,
1196
+ gradient_convergence_tolerance: float = 1e-6,
1197
+ number_of_control_points: int = 4,
1198
+ ) -> dict:
1199
+ """
1200
+ Quick deformable registration function for Biomni integration.
1201
+
1202
+ Args:
1203
+ fixed_image_path: Path to reference image
1204
+ moving_image_path: Path to image to register
1205
+ output_dir: Directory to save results
1206
+ metric: Similarity metric
1207
+ optimizer: Optimization method
1208
+ preprocess: Whether to preprocess images
1209
+ create_visualizations: Whether to create visualizations
1210
+ learning_rate: Learning rate for optimizer
1211
+ number_of_iterations: Maximum iterations
1212
+ gradient_convergence_tolerance: Convergence tolerance
1213
+ number_of_control_points: Number of B-spline control points
1214
+
1215
+ Returns:
1216
+ Dictionary with registration results
1217
+ """
1218
+ tool = ImageRegistrationTool()
1219
+
1220
+ # Load images
1221
+ fixed_image = tool.load_image(fixed_image_path)
1222
+ moving_image = tool.load_image(moving_image_path)
1223
+
1224
+ # Preprocess if requested
1225
+ if preprocess:
1226
+ fixed_image = tool.preprocess_image(fixed_image)
1227
+ moving_image = tool.preprocess_image(moving_image)
1228
+
1229
+ # Create transform and registration method
1230
+ transform = tool.create_deformable_transform(fixed_image, moving_image, number_of_control_points)
1231
+ registration_method = tool.setup_registration_method(
1232
+ transform, metric, optimizer, learning_rate, number_of_iterations, gradient_convergence_tolerance
1233
+ )
1234
+
1235
+ # Perform registration
1236
+ final_transform, registered_image = tool.register_images(fixed_image, moving_image, transform, registration_method)
1237
+
1238
+ # Save results
1239
+ os.makedirs(output_dir, exist_ok=True)
1240
+
1241
+ # Save registered image
1242
+ registered_path = os.path.join(output_dir, "deformable_registered.nii.gz")
1243
+ tool.save_image(registered_image, registered_path)
1244
+
1245
+ # Save transform
1246
+ transform_path = os.path.join(output_dir, "deformable_transform.tfm")
1247
+ sitk.WriteTransform(final_transform, transform_path)
1248
+
1249
+ # Calculate metrics
1250
+ metrics_before = tool.calculate_similarity_metrics(fixed_image, moving_image)
1251
+ metrics_after = tool.calculate_similarity_metrics(fixed_image, registered_image)
1252
+
1253
+ results = {
1254
+ "registered_image_path": registered_path,
1255
+ "transform_path": transform_path,
1256
+ "metrics_before": metrics_before,
1257
+ "metrics_after": metrics_after,
1258
+ "registration_type": "deformable",
1259
+ }
1260
+
1261
+ return results
1262
+
1263
+
1264
+ def batch_register_images(
1265
+ fixed_image_path: str,
1266
+ moving_images_dir: str,
1267
+ output_dir: str,
1268
+ transform_type: str = "rigid",
1269
+ metric: str = "mutual_information",
1270
+ optimizer: str = "gradient_descent",
1271
+ preprocess: bool = True,
1272
+ create_visualizations: bool = True,
1273
+ learning_rate: float = 0.01,
1274
+ number_of_iterations: int = 100,
1275
+ gradient_convergence_tolerance: float = 1e-6,
1276
+ ) -> dict:
1277
+ """
1278
+ Batch registration of multiple images to a single reference.
1279
+
1280
+ Args:
1281
+ fixed_image_path: Path to reference image
1282
+ moving_images_dir: Directory containing images to register
1283
+ output_dir: Directory to save results
1284
+ transform_type: Type of registration ('rigid', 'affine', 'deformable')
1285
+ metric: Similarity metric
1286
+ optimizer: Optimization method
1287
+ preprocess: Whether to preprocess images
1288
+ create_visualizations: Whether to create visualizations
1289
+ learning_rate: Learning rate for optimizer
1290
+ number_of_iterations: Maximum iterations
1291
+ gradient_convergence_tolerance: Convergence tolerance
1292
+
1293
+ Returns:
1294
+ Dictionary with batch registration results
1295
+ """
1296
+ logger.info(f"Starting batch {transform_type} registration...")
1297
+
1298
+ # Find all image files in the directory
1299
+ image_files = []
1300
+ for file in os.listdir(moving_images_dir):
1301
+ if any(file.endswith(ext) for ext in [".nii", ".nii.gz", ".nrrd", ".mha", ".mhd"]):
1302
+ image_files.append(os.path.join(moving_images_dir, file))
1303
+
1304
+ if not image_files:
1305
+ raise ValueError(f"No image files found in {moving_images_dir}")
1306
+
1307
+ logger.info(f"Found {len(image_files)} images to register")
1308
+
1309
+ # Create output directory
1310
+ os.makedirs(output_dir, exist_ok=True)
1311
+
1312
+ # Process each image
1313
+ results = {}
1314
+ ImageRegistrationTool()
1315
+
1316
+ for i, moving_image_path in enumerate(image_files):
1317
+ logger.info(f"Processing {i + 1}/{len(image_files)}: {os.path.basename(moving_image_path)}")
1318
+
1319
+ # Create individual output directory
1320
+ image_name = os.path.splitext(os.path.basename(moving_image_path))[0]
1321
+ if image_name.endswith(".nii"):
1322
+ image_name = os.path.splitext(image_name)[0]
1323
+
1324
+ individual_output_dir = os.path.join(output_dir, f"registration_{image_name}")
1325
+
1326
+ try:
1327
+ if transform_type == "rigid":
1328
+ result = quick_rigid_registration(
1329
+ fixed_image_path,
1330
+ moving_image_path,
1331
+ individual_output_dir,
1332
+ metric,
1333
+ optimizer,
1334
+ preprocess,
1335
+ create_visualizations,
1336
+ learning_rate,
1337
+ number_of_iterations,
1338
+ gradient_convergence_tolerance,
1339
+ )
1340
+ elif transform_type == "affine":
1341
+ result = quick_affine_registration(
1342
+ fixed_image_path,
1343
+ moving_image_path,
1344
+ individual_output_dir,
1345
+ metric,
1346
+ optimizer,
1347
+ preprocess,
1348
+ create_visualizations,
1349
+ learning_rate,
1350
+ number_of_iterations,
1351
+ gradient_convergence_tolerance,
1352
+ )
1353
+ elif transform_type == "deformable":
1354
+ result = quick_deformable_registration(
1355
+ fixed_image_path,
1356
+ moving_image_path,
1357
+ individual_output_dir,
1358
+ metric,
1359
+ optimizer,
1360
+ preprocess,
1361
+ create_visualizations,
1362
+ learning_rate,
1363
+ number_of_iterations,
1364
+ gradient_convergence_tolerance,
1365
+ )
1366
+ else:
1367
+ raise ValueError(f"Unknown transform type: {transform_type}")
1368
+
1369
+ results[image_name] = result
1370
+ logger.info(f"Successfully registered {image_name}")
1371
+
1372
+ except Exception as e:
1373
+ logger.error(f"Failed to register {image_name}: {e}")
1374
+ results[image_name] = {"error": str(e)}
1375
+
1376
+ logger.info(f"Batch registration completed. Processed {len(image_files)} images.")
1377
+ return results
1378
+
1379
+
1380
+ def calculate_similarity_metrics(image1_path: str, image2_path: str) -> dict[str, float]:
1381
+ """
1382
+ Calculate similarity metrics between two images.
1383
+
1384
+ Args:
1385
+ image1_path: Path to first image
1386
+ image2_path: Path to second image
1387
+
1388
+ Returns:
1389
+ Dictionary of similarity metrics
1390
+ """
1391
+ tool = ImageRegistrationTool()
1392
+ image1 = tool.load_image(image1_path)
1393
+ image2 = tool.load_image(image2_path)
1394
+ return tool.calculate_similarity_metrics(image1, image2)
Biomni/biomni/tool/biophysics.py ADDED
@@ -0,0 +1,513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def predict_protein_disorder_regions(protein_sequence, threshold=0.5, output_file="disorder_prediction_results.csv"):
2
+ """Predicts intrinsically disordered regions (IDRs) in a protein sequence using IUPred2A.
3
+
4
+ Parameters
5
+ ----------
6
+ protein_sequence : str
7
+ The amino acid sequence of the protein to analyze
8
+ threshold : float, optional
9
+ The disorder score threshold above which a residue is considered disordered (default: 0.5)
10
+ output_file : str, optional
11
+ Filename to save the per-residue disorder scores (default: "disorder_prediction_results.csv")
12
+
13
+ Returns
14
+ -------
15
+ str
16
+ A research log summarizing the prediction process and results
17
+
18
+ """
19
+ import csv
20
+ import re
21
+
22
+ import requests
23
+
24
+ # Clean the input sequence
25
+ protein_sequence = "".join(re.findall(r"[A-Za-z]", protein_sequence))
26
+
27
+ # Step 1: Submit the sequence to IUPred2A web server
28
+ url = "https://iupred2a.elte.hu/iupred2a"
29
+ payload = {
30
+ "seq": protein_sequence,
31
+ "iupred2": "long", # Use IUPred2 long disorder prediction
32
+ "anchor2": "no", # Don't use ANCHOR2 prediction
33
+ }
34
+
35
+ try:
36
+ response = requests.post(url, data=payload)
37
+ response.raise_for_status()
38
+ except requests.exceptions.RequestException as e:
39
+ return f"Error accessing IUPred2A server: {str(e)}"
40
+
41
+ # Step 2: Parse the results to extract disorder scores
42
+ result_lines = response.text.split("\n")
43
+ scores = []
44
+
45
+ for line in result_lines:
46
+ if line.startswith("#") or not line.strip():
47
+ continue
48
+ parts = line.split()
49
+ if len(parts) >= 3:
50
+ try:
51
+ position = int(parts[0])
52
+ residue = parts[1]
53
+ score = float(parts[2])
54
+ scores.append((position, residue, score))
55
+ except (ValueError, IndexError):
56
+ continue
57
+
58
+ if not scores:
59
+ return "No valid prediction data was returned from the server."
60
+
61
+ # Step 3: Identify disordered regions
62
+ disordered_regions = []
63
+ current_region = []
64
+
65
+ for pos, _, score in scores:
66
+ if score >= threshold:
67
+ if not current_region:
68
+ current_region = [pos]
69
+ elif pos == current_region[-1] + 1:
70
+ current_region.append(pos)
71
+ else:
72
+ if len(current_region) > 1:
73
+ disordered_regions.append((current_region[0], current_region[-1]))
74
+ current_region = [pos]
75
+ elif current_region and len(current_region) > 1:
76
+ disordered_regions.append((current_region[0], current_region[-1]))
77
+ current_region = []
78
+ elif current_region:
79
+ current_region = []
80
+
81
+ # Add the last region if it exists
82
+ if current_region and len(current_region) > 1:
83
+ disordered_regions.append((current_region[0], current_region[-1]))
84
+
85
+ # Step 4: Save results to CSV
86
+ with open(output_file, "w", newline="") as f:
87
+ writer = csv.writer(f)
88
+ writer.writerow(["Position", "Amino_Acid", "Disorder_Score", "Is_Disordered"])
89
+ for pos, aa, score in scores:
90
+ is_disordered = "Yes" if score >= threshold else "No"
91
+ writer.writerow([pos, aa, score, is_disordered])
92
+
93
+ # Step 5: Generate the research log
94
+ total_residues = len(scores)
95
+ disordered_count = sum(1 for _, _, score in scores if score >= threshold)
96
+ disordered_percentage = (disordered_count / total_residues) * 100 if total_residues > 0 else 0
97
+
98
+ log = f"""
99
+ Intrinsically Disordered Region (IDR) Prediction Research Log:
100
+ =============================================================
101
+ Analysis performed using IUPred2A algorithm (long disorder mode)
102
+ Protein sequence length: {total_residues} amino acids
103
+ Disorder threshold: {threshold}
104
+
105
+ Results Summary:
106
+ - {disordered_count} residues ({disordered_percentage:.2f}%) predicted as disordered
107
+ - {len(disordered_regions)} distinct disordered regions identified
108
+
109
+ Disordered Regions:
110
+ """
111
+
112
+ if disordered_regions:
113
+ for start, end in disordered_regions:
114
+ length = end - start + 1
115
+ log += f"- Region {start}-{end} (length: {length} residues)\n"
116
+ else:
117
+ log += "- No significant disordered regions found\n"
118
+
119
+ log += f"\nDetailed per-residue scores saved to: {output_file}"
120
+
121
+ return log
122
+
123
+
124
+ def analyze_cell_morphology_and_cytoskeleton(image_path, output_dir="./results", threshold_method="otsu"):
125
+ """Quantifies cell morphology and cytoskeletal organization from fluorescence microscopy images.
126
+
127
+ Parameters
128
+ ----------
129
+ image_path : str
130
+ Path to the fluorescence microscopy image file
131
+ output_dir : str, optional
132
+ Directory to save output files (default: './results')
133
+ threshold_method : str, optional
134
+ Method for cell segmentation ('otsu', 'adaptive', or 'manual') (default: 'otsu')
135
+
136
+ Returns
137
+ -------
138
+ str
139
+ Research log summarizing the analysis steps and results
140
+
141
+ """
142
+ import os
143
+ from datetime import datetime
144
+
145
+ import cv2
146
+ import numpy as np
147
+ import pandas as pd
148
+ from skimage import exposure, feature, filters, io, measure, morphology
149
+ from skimage.color import rgb2gray
150
+
151
+ # Create output directory if it doesn't exist
152
+ os.makedirs(output_dir, exist_ok=True)
153
+
154
+ # Start log
155
+ log = f"Cell Morphology and Cytoskeleton Analysis Log - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
156
+ log += f"Analyzing image: {image_path}\n\n"
157
+
158
+ # Load image
159
+ log += "Step 1: Loading and preprocessing image\n"
160
+ try:
161
+ image = io.imread(image_path)
162
+ # Convert to grayscale if RGB
163
+ if len(image.shape) > 2:
164
+ gray_image = rgb2gray(image)
165
+ log += "- Converted RGB image to grayscale\n"
166
+ else:
167
+ gray_image = image
168
+
169
+ # Enhance contrast
170
+ gray_image = exposure.equalize_hist(gray_image)
171
+ log += "- Enhanced image contrast\n"
172
+ except Exception as e:
173
+ return f"Error loading image: {str(e)}"
174
+
175
+ # Segment cells
176
+ log += "\nStep 2: Segmenting cells from background\n"
177
+ if threshold_method == "otsu":
178
+ thresh = filters.threshold_otsu(gray_image)
179
+ binary = gray_image > thresh
180
+ log += f"- Applied Otsu thresholding (threshold value: {thresh:.4f})\n"
181
+ elif threshold_method == "adaptive":
182
+ binary = filters.threshold_local(gray_image, block_size=35, offset=0.05)
183
+ binary = gray_image > binary
184
+ log += "- Applied adaptive thresholding\n"
185
+ else: # manual
186
+ thresh = 0.5 # Default value, can be parameterized
187
+ binary = gray_image > thresh
188
+ log += f"- Applied manual thresholding (threshold value: {thresh:.4f})\n"
189
+
190
+ # Clean up binary image
191
+ binary = morphology.remove_small_objects(binary, min_size=100)
192
+ binary = morphology.remove_small_holes(binary, area_threshold=100)
193
+ binary = morphology.binary_closing(binary, morphology.disk(3))
194
+ log += "- Applied morphological operations to clean segmentation\n"
195
+
196
+ # Label cells
197
+ labeled_cells, num_cells = measure.label(binary, return_num=True)
198
+ log += f"- Identified {num_cells} cell regions\n"
199
+
200
+ # Analyze cell properties
201
+ log += "\nStep 3: Analyzing cell morphology\n"
202
+ cell_props = measure.regionprops_table(
203
+ labeled_cells,
204
+ gray_image,
205
+ properties=(
206
+ "area",
207
+ "perimeter",
208
+ "major_axis_length",
209
+ "minor_axis_length",
210
+ "eccentricity",
211
+ "orientation",
212
+ "solidity",
213
+ ),
214
+ )
215
+
216
+ # Convert to DataFrame for easier manipulation
217
+ cell_df = pd.DataFrame(cell_props)
218
+
219
+ # Calculate additional metrics
220
+ if len(cell_df) > 0:
221
+ cell_df["aspect_ratio"] = cell_df["major_axis_length"] / cell_df["minor_axis_length"]
222
+ cell_df["circularity"] = (4 * np.pi * cell_df["area"]) / (cell_df["perimeter"] ** 2)
223
+
224
+ # Summary statistics
225
+ log += f"- Average cell area: {cell_df['area'].mean():.2f} pixels\n"
226
+ log += f"- Average aspect ratio: {cell_df['aspect_ratio'].mean():.2f}\n"
227
+ log += f"- Average circularity: {cell_df['circularity'].mean():.2f}\n"
228
+ log += f"- Average eccentricity: {cell_df['eccentricity'].mean():.2f}\n"
229
+ else:
230
+ log += "- No cells detected for morphological analysis\n"
231
+
232
+ # Analyze cytoskeletal organization
233
+ log += "\nStep 4: Analyzing cytoskeletal organization\n"
234
+
235
+ # Edge detection to highlight cytoskeletal fibers
236
+ edges = feature.canny(gray_image, sigma=2)
237
+
238
+ # Use Hough transform to detect lines (cytoskeletal fibers)
239
+ if np.any(edges):
240
+ lines = cv2.HoughLinesP(
241
+ edges.astype(np.uint8),
242
+ 1,
243
+ np.pi / 180,
244
+ threshold=10,
245
+ minLineLength=10,
246
+ maxLineGap=5,
247
+ )
248
+
249
+ if lines is not None:
250
+ # Calculate line orientations
251
+ orientations = []
252
+ for line in lines:
253
+ x1, y1, x2, y2 = line[0]
254
+ if x2 - x1 != 0: # Avoid division by zero
255
+ angle = np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi
256
+ orientations.append(angle)
257
+
258
+ if orientations:
259
+ # Convert to numpy array for calculations
260
+ orientations = np.array(orientations)
261
+
262
+ # Calculate alignment metrics
263
+ mean_orientation = np.mean(orientations)
264
+ # Normalize angles to -90 to 90 degrees
265
+ norm_angles = np.mod(orientations + 90, 180) - 90
266
+ std_orientation = np.std(norm_angles)
267
+
268
+ # Order parameter (measure of alignment, 1 = perfectly aligned, 0 = random)
269
+ # Convert angles to radians for calculation
270
+ rad_angles = np.radians(norm_angles)
271
+ order_parameter = np.sqrt(np.mean(np.cos(2 * rad_angles)) ** 2 + np.mean(np.sin(2 * rad_angles)) ** 2)
272
+
273
+ log += f"- Detected {len(orientations)} cytoskeletal fibers\n"
274
+ log += f"- Mean fiber orientation: {mean_orientation:.2f} degrees\n"
275
+ log += f"- Standard deviation of orientation: {std_orientation:.2f} degrees\n"
276
+ log += f"- Order parameter (alignment): {order_parameter:.4f} (0=random, 1=aligned)\n"
277
+
278
+ # Add fiber data to dataframe
279
+ fiber_df = pd.DataFrame({"fiber_orientation": orientations})
280
+ else:
281
+ log += "- No fiber orientations could be calculated\n"
282
+ fiber_df = pd.DataFrame()
283
+ else:
284
+ log += "- No cytoskeletal fibers detected\n"
285
+ fiber_df = pd.DataFrame()
286
+ else:
287
+ log += "- No edges detected for cytoskeletal analysis\n"
288
+ fiber_df = pd.DataFrame()
289
+
290
+ # Save results
291
+ log += "\nStep 5: Saving results\n"
292
+
293
+ # Save cell morphology data
294
+ if len(cell_df) > 0:
295
+ cell_csv_path = os.path.join(output_dir, "cell_morphology_data.csv")
296
+ cell_df.to_csv(cell_csv_path, index=False)
297
+ log += f"- Cell morphology data saved to: {cell_csv_path}\n"
298
+
299
+ # Save fiber orientation data
300
+ if len(fiber_df) > 0:
301
+ fiber_csv_path = os.path.join(output_dir, "fiber_orientation_data.csv")
302
+ fiber_df.to_csv(fiber_csv_path, index=False)
303
+ log += f"- Fiber orientation data saved to: {fiber_csv_path}\n"
304
+
305
+ # Save segmentation image
306
+ if num_cells > 0:
307
+ segmentation_path = os.path.join(output_dir, "cell_segmentation.png")
308
+ io.imsave(segmentation_path, labeled_cells.astype(np.uint8) * 50)
309
+ log += f"- Cell segmentation image saved to: {segmentation_path}\n"
310
+
311
+ # Summary
312
+ log += "\nAnalysis Summary:\n"
313
+ log += f"- Processed image: {image_path}\n"
314
+ log += f"- Detected {num_cells} cells\n"
315
+ if len(cell_df) > 0:
316
+ log += f"- Cell size range: {cell_df['area'].min():.1f} to {cell_df['area'].max():.1f} pixels\n"
317
+ log += f"- Cell shape: average aspect ratio = {cell_df['aspect_ratio'].mean():.2f}\n"
318
+ if "order_parameter" in locals():
319
+ log += f"- Cytoskeletal organization: alignment parameter = {order_parameter:.4f}\n"
320
+
321
+ return log
322
+
323
+
324
+ def analyze_tissue_deformation_flow(image_sequence, output_dir="results", pixel_scale=1.0):
325
+ """Quantify tissue deformation and flow dynamics from microscopy image sequence.
326
+
327
+ Parameters
328
+ ----------
329
+ image_sequence : list or numpy.ndarray
330
+ Sequence of microscopy images (either a list of file paths or a 3D numpy array [time, height, width])
331
+ output_dir : str, optional
332
+ Directory to save results (default: "results")
333
+ pixel_scale : float, optional
334
+ Physical scale of pixels (e.g., μm/pixel) for proper scaling of metrics (default: 1.0)
335
+
336
+ Returns
337
+ -------
338
+ str
339
+ Research log summarizing the analysis steps and results
340
+
341
+ """
342
+ import os
343
+ from datetime import datetime
344
+
345
+ import cv2
346
+ import numpy as np
347
+
348
+ # Create output directory if it doesn't exist
349
+ os.makedirs(output_dir, exist_ok=True)
350
+
351
+ # Load images if paths are provided
352
+ if isinstance(image_sequence[0], str):
353
+ loaded_images = []
354
+ for img_path in image_sequence:
355
+ img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
356
+ if img is None:
357
+ return f"Error: Could not load image {img_path}"
358
+ loaded_images.append(img)
359
+ frames = np.array(loaded_images)
360
+ else:
361
+ frames = image_sequence
362
+ # Convert to grayscale if needed
363
+ if len(frames.shape) > 3: # Has color channels
364
+ frames = np.array(
365
+ [cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) if frame.shape[-1] == 3 else frame for frame in frames]
366
+ )
367
+
368
+ # Parameters for optical flow
369
+ lk_params = {
370
+ "winSize": (15, 15),
371
+ "maxLevel": 2,
372
+ "criteria": (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03),
373
+ }
374
+
375
+ # Metrics storage
376
+ num_frames = len(frames)
377
+ flow_fields = []
378
+ divergence_maps = []
379
+ curl_maps = []
380
+ strain_maps = []
381
+
382
+ # Log initialization
383
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
384
+ log = f"Tissue Deformation and Flow Analysis - {timestamp}\n"
385
+ log += f"Number of frames analyzed: {num_frames}\n"
386
+ log += f"Image dimensions: {frames[0].shape[0]}x{frames[0].shape[1]} pixels\n"
387
+ log += f"Pixel scale: {pixel_scale} units/pixel\n\n"
388
+ log += "Analysis Steps:\n"
389
+
390
+ # Create feature points grid (evenly spaced points)
391
+ y, x = np.mgrid[0 : frames[0].shape[0] : 20, 0 : frames[0].shape[1] : 20]
392
+ feature_points = np.stack((x.flatten(), y.flatten()), axis=1).astype(np.float32)
393
+
394
+ for i in range(num_frames - 1):
395
+ log += f"Processing frame pair {i} and {i + 1}...\n"
396
+
397
+ # Calculate optical flow using Lucas-Kanade
398
+ prev_frame = frames[i]
399
+ next_frame = frames[i + 1]
400
+
401
+ # Compute flow for the grid points
402
+ next_points, status, _ = cv2.calcOpticalFlowPyrLK(prev_frame, next_frame, feature_points, None, **lk_params)
403
+
404
+ # Filter valid points
405
+ valid_idx = status.flatten() == 1
406
+ valid_prev_points = feature_points[valid_idx]
407
+ valid_next_points = next_points[valid_idx]
408
+
409
+ # Calculate displacement vectors
410
+ displacement = valid_next_points - valid_prev_points
411
+ points = valid_prev_points
412
+
413
+ # Interpolate flow field to full image resolution
414
+ flow_field = np.zeros((frames[0].shape[0], frames[0].shape[1], 2), dtype=np.float32)
415
+
416
+ # Simple nearest-neighbor interpolation for demonstration
417
+ # In a production system, you might use more sophisticated interpolation
418
+ for j, (x, y) in enumerate(points.astype(int)):
419
+ if 0 <= y < flow_field.shape[0] and 0 <= x < flow_field.shape[1]:
420
+ flow_field[y, x] = displacement[j]
421
+
422
+ # Calculate derivatives for deformation analysis
423
+ u = flow_field[:, :, 0] # x-component of flow
424
+ v = flow_field[:, :, 1] # y-component of flow
425
+
426
+ # Calculate divergence (expansion/contraction)
427
+ # Using central differences for derivatives
428
+ u_x = cv2.Sobel(u, cv2.CV_64F, 1, 0, ksize=3) / (8.0 * pixel_scale)
429
+ v_y = cv2.Sobel(v, cv2.CV_64F, 0, 1, ksize=3) / (8.0 * pixel_scale)
430
+ divergence = u_x + v_y
431
+
432
+ # Calculate curl (rotation)
433
+ u_y = cv2.Sobel(u, cv2.CV_64F, 0, 1, ksize=3) / (8.0 * pixel_scale)
434
+ v_x = cv2.Sobel(v, cv2.CV_64F, 1, 0, ksize=3) / (8.0 * pixel_scale)
435
+ curl = v_x - u_y
436
+
437
+ # Calculate strain tensor components
438
+ strain_xx = u_x
439
+ strain_yy = v_y
440
+ strain_xy = 0.5 * (u_y + v_x)
441
+
442
+ # Magnitude of strain tensor (Frobenius norm)
443
+ strain_magnitude = np.sqrt(strain_xx**2 + strain_yy**2 + 2 * strain_xy**2)
444
+
445
+ # Store results
446
+ flow_fields.append(flow_field)
447
+ divergence_maps.append(divergence)
448
+ curl_maps.append(curl)
449
+ strain_maps.append(strain_magnitude)
450
+
451
+ # Visualize flow field
452
+ flow_viz = np.zeros((frames[0].shape[0], frames[0].shape[1], 3), dtype=np.uint8)
453
+ flow_viz[..., 0] = next_frame # Use next frame as background
454
+ flow_viz[..., 1] = next_frame
455
+ flow_viz[..., 2] = next_frame
456
+
457
+ # Draw flow vectors for visualization
458
+ step = 20
459
+ for y in range(0, flow_field.shape[0], step):
460
+ for x in range(0, flow_field.shape[1], step):
461
+ dx, dy = flow_field[y, x]
462
+ if abs(dx) > 0.5 or abs(dy) > 0.5: # Only draw significant flow
463
+ cv2.arrowedLine(
464
+ flow_viz,
465
+ (x, y),
466
+ (int(x + dx), int(y + dy)),
467
+ (0, 255, 0), # Green
468
+ 1,
469
+ tipLength=0.3,
470
+ )
471
+
472
+ # Save visualizations
473
+ flow_viz_path = os.path.join(output_dir, f"flow_viz_{i:03d}.png")
474
+ divergence_path = os.path.join(output_dir, f"divergence_{i:03d}.npy")
475
+ curl_path = os.path.join(output_dir, f"curl_{i:03d}.npy")
476
+ strain_path = os.path.join(output_dir, f"strain_{i:03d}.npy")
477
+
478
+ cv2.imwrite(flow_viz_path, flow_viz)
479
+ np.save(divergence_path, divergence)
480
+ np.save(curl_path, curl)
481
+ np.save(strain_path, strain_magnitude)
482
+
483
+ # Calculate summary statistics
484
+ mean_divergence = np.mean([np.mean(div) for div in divergence_maps])
485
+ max_divergence = np.max([np.max(div) for div in divergence_maps])
486
+ mean_curl = np.mean([np.mean(np.abs(c)) for c in curl_maps])
487
+ mean_strain = np.mean([np.mean(s) for s in strain_maps])
488
+
489
+ # Add summary to log
490
+ log += "\nAnalysis Results:\n"
491
+ log += f"Mean tissue divergence: {mean_divergence:.6f} (expansion/contraction rate)\n"
492
+ log += f"Maximum divergence: {max_divergence:.6f}\n"
493
+ log += f"Mean absolute curl: {mean_curl:.6f} (rotation rate)\n"
494
+ log += f"Mean strain magnitude: {mean_strain:.6f} (deformation intensity)\n\n"
495
+
496
+ # Save summary data
497
+ summary_data = {
498
+ "mean_divergence": mean_divergence,
499
+ "max_divergence": max_divergence,
500
+ "mean_curl": mean_curl,
501
+ "mean_strain": mean_strain,
502
+ }
503
+ summary_path = os.path.join(output_dir, "deformation_summary.npy")
504
+ np.save(summary_path, summary_data)
505
+
506
+ log += "Files Generated:\n"
507
+ log += f"- Flow visualization images: {output_dir}/flow_viz_*.png\n"
508
+ log += f"- Divergence maps: {output_dir}/divergence_*.npy\n"
509
+ log += f"- Curl maps: {output_dir}/curl_*.npy\n"
510
+ log += f"- Strain maps: {output_dir}/strain_*.npy\n"
511
+ log += f"- Summary statistics: {output_dir}/deformation_summary.npy\n"
512
+
513
+ return log
Biomni/biomni/tool/cell_biology.py ADDED
@@ -0,0 +1,742 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def quantify_cell_cycle_phases_from_microscopy(image_paths, output_dir="./results"):
2
+ """Quantify the percentage of cells in each cell cycle phase using Calcofluor white stained microscopy images.
3
+
4
+ This function processes microscopy images where cell walls/septa are stained with Calcofluor white,
5
+ segments individual cells, extracts features, and classifies each cell into G1, S, or G2/M phase.
6
+
7
+ Parameters
8
+ ----------
9
+ image_paths : list of str
10
+ List of file paths to microscopy images of cells stained with Calcofluor white
11
+ output_dir : str, optional
12
+ Directory to save results (default: './results')
13
+
14
+ Returns
15
+ -------
16
+ str
17
+ Research log summarizing the analysis process and results
18
+
19
+ """
20
+ import os
21
+
22
+ import numpy as np
23
+ import pandas as pd
24
+ from skimage import filters, io, measure, morphology
25
+
26
+ # Create output directory if it doesn't exist
27
+ os.makedirs(output_dir, exist_ok=True)
28
+
29
+ # Initialize log
30
+ log = "# Cell Cycle Phase Quantification Analysis Log\n\n"
31
+ log += f"Processing {len(image_paths)} microscopy images with Calcofluor white staining.\n\n"
32
+
33
+ # Initialize data structures to store results
34
+ all_cells = []
35
+ cell_features = []
36
+
37
+ # Step 1: Process images and segment cells
38
+ log += "## Image Processing and Cell Segmentation\n\n"
39
+
40
+ for i, img_path in enumerate(image_paths):
41
+ log += f"Processing image {i + 1}/{len(image_paths)}: {os.path.basename(img_path)}\n"
42
+
43
+ # Load image
44
+ img = io.imread(img_path)
45
+
46
+ # Convert to grayscale if needed
47
+ if len(img.shape) > 2:
48
+ img = np.mean(img, axis=2).astype(np.uint8)
49
+
50
+ # Preprocessing
51
+ filtered_img = filters.gaussian(img, sigma=1.0)
52
+
53
+ # Thresholding to identify cell walls
54
+ threshold = filters.threshold_otsu(filtered_img)
55
+ binary = filtered_img > threshold
56
+
57
+ # Clean up binary image
58
+ binary = morphology.remove_small_objects(binary, min_size=30)
59
+ binary = morphology.binary_closing(binary)
60
+
61
+ # Segment cells
62
+ labeled_cells = measure.label(binary)
63
+ cell_props = measure.regionprops(labeled_cells, intensity_image=img)
64
+
65
+ log += f" - Found {len(cell_props)} cells\n"
66
+ all_cells.extend(cell_props)
67
+
68
+ # Extract features for each cell
69
+ for cell in cell_props:
70
+ # Basic morphological features
71
+ area = cell.area
72
+ perimeter = cell.perimeter
73
+ eccentricity = cell.eccentricity
74
+ mean_intensity = cell.mean_intensity
75
+
76
+ # Cell shape features
77
+ circularity = (4 * np.pi * area) / (perimeter**2) if perimeter > 0 else 0
78
+
79
+ # Check for septation (indicative of S or G2/M phase)
80
+ # Calcofluor white stains the septum more intensely
81
+ has_septum = False
82
+ if area > 50: # Only check larger cells
83
+ intensity_profile = cell.intensity_image[cell.intensity_image > 0]
84
+ if intensity_profile.size > 0:
85
+ intensity_std = np.std(intensity_profile)
86
+ intensity_max = np.max(intensity_profile)
87
+ # High standard deviation and max intensity suggests septum presence
88
+ has_septum = intensity_std > 0.2 * np.mean(intensity_profile) and intensity_max > 1.5 * np.mean(
89
+ intensity_profile
90
+ )
91
+
92
+ # Store features
93
+ cell_features.append(
94
+ [
95
+ area,
96
+ perimeter,
97
+ eccentricity,
98
+ mean_intensity,
99
+ circularity,
100
+ has_septum,
101
+ i, # include image index
102
+ ]
103
+ )
104
+
105
+ log += f"\nTotal cells detected across all images: {len(all_cells)}\n\n"
106
+
107
+ # Step 2: Feature engineering and classification
108
+ log += "## Cell Cycle Phase Classification\n\n"
109
+
110
+ # Convert to numpy array for easier processing
111
+ X = np.array(cell_features)
112
+
113
+ # Simple rule-based classification
114
+ # This is a simplified approach - in practice, you would train a classifier on labeled data
115
+ # G1: Smaller cells, no septum
116
+ # S: Medium-sized cells, early septum formation
117
+ # G2/M: Larger cells, clear septum, often elongated
118
+
119
+ # Define classification rules based on features
120
+ phases = []
121
+ for i in range(X.shape[0]):
122
+ area = X[i, 0]
123
+ perimeter = X[i, 1]
124
+ eccentricity = X[i, 2]
125
+ has_septum = X[i, 5]
126
+
127
+ if has_septum and area > np.median(X[:, 0]) * 1.2:
128
+ # Larger cells with septum are likely in G2/M
129
+ phases.append("G2/M")
130
+ elif has_septum or (area > np.median(X[:, 0]) and eccentricity > 0.5):
131
+ # Cells with septum or larger elongated cells are likely in S
132
+ phases.append("S")
133
+ else:
134
+ # Smaller cells without septum are likely in G1
135
+ phases.append("G1")
136
+
137
+ # Calculate percentages
138
+ unique_phases, counts = np.unique(phases, return_counts=True)
139
+ percentages = (counts / len(phases)) * 100
140
+
141
+ # Create results table
142
+ results_df = pd.DataFrame({"Phase": unique_phases, "Count": counts, "Percentage": percentages})
143
+
144
+ # Save results
145
+ results_path = os.path.join(output_dir, "cell_cycle_phases.csv")
146
+ results_df.to_csv(results_path, index=False)
147
+
148
+ # Log results
149
+ log += "### Cell Cycle Phase Distribution\n\n"
150
+ for phase, count, percentage in zip(unique_phases, counts, percentages, strict=False):
151
+ log += f"- {phase}: {count} cells ({percentage:.2f}%)\n"
152
+
153
+ log += f"\nResults saved to: {results_path}\n"
154
+ log += "\nNote: This analysis uses simplified morphological features for classification. "
155
+ log += (
156
+ "For more accurate results, a supervised machine learning approach with labeled training data is recommended.\n"
157
+ )
158
+
159
+ return log
160
+
161
+
162
+ def quantify_and_cluster_cell_motility(image_sequence_path, output_dir="./results", num_clusters=3):
163
+ """Quantify cell motility features from time-lapse microscopy images and cluster cells based on motility patterns.
164
+
165
+ Parameters
166
+ ----------
167
+ image_sequence_path : str
168
+ Path to directory containing time-lapse microscopy images in sequential order
169
+ output_dir : str
170
+ Directory to save output files (default: "./results")
171
+ num_clusters : int
172
+ Number of motility pattern clusters to identify (default: 3)
173
+
174
+ Returns
175
+ -------
176
+ str
177
+ Research log summarizing the analysis process and results
178
+
179
+ """
180
+ import os
181
+
182
+ import cv2
183
+ import numpy as np
184
+ import pandas as pd
185
+ from sklearn.cluster import KMeans
186
+
187
+ # Create output directory if it doesn't exist
188
+ os.makedirs(output_dir, exist_ok=True)
189
+
190
+ # Step 1: Load image sequence
191
+ image_files = sorted(
192
+ [f for f in os.listdir(image_sequence_path) if f.endswith((".tif", ".tiff", ".png", ".jpg", ".jpeg"))]
193
+ )
194
+
195
+ if len(image_files) < 2:
196
+ return "Error: Insufficient images found. At least 2 time points are required."
197
+
198
+ # Step 2: Initialize cell tracking
199
+ first_image = cv2.imread(os.path.join(image_sequence_path, image_files[0]), cv2.IMREAD_GRAYSCALE)
200
+
201
+ # Simple cell detection using thresholding and contour detection
202
+ _, binary = cv2.threshold(first_image, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
203
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
204
+
205
+ # Extract cell centroids from first frame
206
+ cells = []
207
+ for i, contour in enumerate(contours):
208
+ if cv2.contourArea(contour) > 50: # Filter small noise
209
+ M = cv2.moments(contour)
210
+ if M["m00"] != 0:
211
+ cx = int(M["m10"] / M["m00"])
212
+ cy = int(M["m01"] / M["m00"])
213
+ cells.append({"id": i, "positions": [(cx, cy)], "frame_indices": [0]})
214
+
215
+ # Step 3: Track cells across frames
216
+ for frame_idx, img_file in enumerate(image_files[1:], 1):
217
+ img = cv2.imread(os.path.join(image_sequence_path, img_file), cv2.IMREAD_GRAYSCALE)
218
+ _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
219
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
220
+
221
+ # Get current frame centroids
222
+ current_centroids = []
223
+ for contour in contours:
224
+ if cv2.contourArea(contour) > 50:
225
+ M = cv2.moments(contour)
226
+ if M["m00"] != 0:
227
+ cx = int(M["m10"] / M["m00"])
228
+ cy = int(M["m01"] / M["m00"])
229
+ current_centroids.append((cx, cy))
230
+
231
+ # Match cells with nearest centroids in current frame
232
+ for cell in cells:
233
+ if len(cell["positions"]) == frame_idx: # Only track cells found in previous frame
234
+ prev_pos = cell["positions"][-1]
235
+
236
+ if current_centroids:
237
+ # Calculate distances to all current centroids
238
+ distances = [
239
+ np.sqrt((prev_pos[0] - c[0]) ** 2 + (prev_pos[1] - c[1]) ** 2) for c in current_centroids
240
+ ]
241
+ min_idx = np.argmin(distances)
242
+
243
+ # Only match if distance is below threshold (prevents incorrect matching)
244
+ if distances[min_idx] < 50: # Threshold distance in pixels
245
+ cell["positions"].append(current_centroids[min_idx])
246
+ cell["frame_indices"].append(frame_idx)
247
+ current_centroids.pop(min_idx) # Remove matched centroid
248
+
249
+ # Step 4: Calculate motility features for each cell
250
+ cell_features = []
251
+
252
+ for cell in cells:
253
+ # Only analyze cells tracked for at least 3 frames
254
+ if len(cell["positions"]) < 3:
255
+ continue
256
+
257
+ # Calculate displacements between consecutive positions
258
+ displacements = []
259
+ for i in range(1, len(cell["positions"])):
260
+ prev_pos = cell["positions"][i - 1]
261
+ curr_pos = cell["positions"][i]
262
+ displacement = np.sqrt((curr_pos[0] - prev_pos[0]) ** 2 + (curr_pos[1] - prev_pos[1]) ** 2)
263
+ displacements.append(displacement)
264
+
265
+ # Calculate speed (pixels per frame)
266
+ avg_speed = np.mean(displacements)
267
+
268
+ # Calculate directionality (displacement from start to end / total path length)
269
+ start_pos = cell["positions"][0]
270
+ end_pos = cell["positions"][-1]
271
+ net_displacement = np.sqrt((end_pos[0] - start_pos[0]) ** 2 + (end_pos[1] - start_pos[1]) ** 2)
272
+ total_path_length = np.sum(displacements)
273
+ directionality = net_displacement / total_path_length if total_path_length > 0 else 0
274
+
275
+ # Calculate mean squared displacement
276
+ msd = np.mean(
277
+ [
278
+ np.sqrt((cell["positions"][i][0] - start_pos[0]) ** 2 + (cell["positions"][i][1] - start_pos[1]) ** 2)
279
+ for i in range(1, len(cell["positions"]))
280
+ ]
281
+ )
282
+
283
+ # Calculate track duration
284
+ duration = cell["frame_indices"][-1] - cell["frame_indices"][0]
285
+
286
+ cell_features.append(
287
+ {
288
+ "cell_id": cell["id"],
289
+ "avg_speed": avg_speed,
290
+ "directionality": directionality,
291
+ "msd": msd,
292
+ "track_duration": duration,
293
+ "track_length": len(cell["positions"]),
294
+ }
295
+ )
296
+
297
+ # Step 5: Cluster cells based on motility features
298
+ if len(cell_features) < num_clusters:
299
+ return f"Error: Not enough cells ({len(cell_features)}) to form {num_clusters} clusters. Try reducing num_clusters."
300
+
301
+ # Create feature matrix for clustering
302
+ feature_df = pd.DataFrame(cell_features)
303
+ feature_matrix = feature_df[["avg_speed", "directionality", "msd"]].values
304
+
305
+ # Normalize features
306
+ feature_matrix = (feature_matrix - np.mean(feature_matrix, axis=0)) / np.std(feature_matrix, axis=0)
307
+
308
+ # Perform k-means clustering
309
+ kmeans = KMeans(n_clusters=num_clusters, random_state=42)
310
+ clusters = kmeans.fit_predict(feature_matrix)
311
+
312
+ # Add cluster assignments to dataframe
313
+ feature_df["cluster"] = clusters
314
+
315
+ # Save results to CSV
316
+ results_file = os.path.join(output_dir, "cell_motility_features.csv")
317
+ feature_df.to_csv(results_file, index=False)
318
+
319
+ # Calculate cluster statistics
320
+ cluster_stats = feature_df.groupby("cluster").agg(
321
+ {
322
+ "avg_speed": ["mean", "std"],
323
+ "directionality": ["mean", "std"],
324
+ "msd": ["mean", "std"],
325
+ "cell_id": "count",
326
+ }
327
+ )
328
+
329
+ # Save cluster statistics
330
+ stats_file = os.path.join(output_dir, "cluster_statistics.csv")
331
+ cluster_stats.to_csv(stats_file)
332
+
333
+ # Create research log
334
+ log = f"""
335
+ Cell Motility Quantification and Clustering Analysis
336
+ ===================================================
337
+
338
+ Analysis Steps:
339
+ 1. Loaded {len(image_files)} time-lapse microscopy images
340
+ 2. Detected and tracked {len(cells)} initial cells across time frames
341
+ 3. Calculated motility features for {len(cell_features)} cells with sufficient tracking data
342
+ 4. Performed k-means clustering to identify {num_clusters} distinct motility patterns
343
+
344
+ Results Summary:
345
+ - Detected {len(cells)} cells in the first frame
346
+ - Successfully tracked {len(cell_features)} cells across multiple frames
347
+ - Clustered cells into {num_clusters} motility pattern groups
348
+
349
+ Cluster Statistics:
350
+ {cluster_stats.to_string()}
351
+
352
+ Output Files:
353
+ - Cell motility features saved to: {results_file}
354
+ - Cluster statistics saved to: {stats_file}
355
+
356
+ Analysis Notes:
357
+ - Features used for clustering: average speed, directionality, and mean squared displacement
358
+ - Cells were required to be tracked for at least 3 frames to be included in the analysis
359
+ """
360
+
361
+ # Save research log
362
+ log_file = os.path.join(output_dir, "research_log.txt")
363
+ with open(log_file, "w") as f:
364
+ f.write(log)
365
+
366
+ return log
367
+
368
+
369
+ def perform_facs_cell_sorting(
370
+ cell_suspension_data,
371
+ fluorescence_parameter,
372
+ threshold_min=None,
373
+ threshold_max=None,
374
+ output_file="sorted_cells.csv",
375
+ ):
376
+ """Performs Fluorescence-Activated Cell Sorting (FACS) to enrich cell populations based on fluorescence characteristics.
377
+
378
+ Parameters
379
+ ----------
380
+ cell_suspension_data : str
381
+ Path to the FCS file containing flow cytometry data
382
+ fluorescence_parameter : str
383
+ The fluorescence parameter to use for sorting (e.g., 'GFP', 'FITC', 'PE')
384
+ threshold_min : float, optional
385
+ Minimum threshold for the fluorescence parameter. Cells below this value will be excluded
386
+ threshold_max : float, optional
387
+ Maximum threshold for the fluorescence parameter. Cells above this value will be excluded
388
+ output_file : str, optional
389
+ Filename to save the sorted cell population data
390
+
391
+ Returns
392
+ -------
393
+ str
394
+ Research log detailing the FACS cell sorting process
395
+
396
+ """
397
+ import os
398
+
399
+ import pandas as pd
400
+
401
+ # Initialize research log
402
+ log = "# FACS-based Cell Sorting and Enrichment Research Log\n\n"
403
+
404
+ try:
405
+ # Load cell suspension data
406
+ log += "## Loading Cell Suspension Data\n"
407
+ if isinstance(cell_suspension_data, str):
408
+ # If data is provided as a file path
409
+ if cell_suspension_data.lower().endswith(".fcs"):
410
+ try:
411
+ import flowkit as fk
412
+
413
+ fcs_data = fk.Sample(cell_suspension_data)
414
+ cell_df = pd.DataFrame(fcs_data.get_dataframe())
415
+ log += f"Successfully loaded FCS file containing {len(cell_df)} cells\n"
416
+ except ImportError:
417
+ # Fallback if flowkit is not available
418
+ log += "FlowKit not available, attempting to load as CSV\n"
419
+ cell_df = pd.read_csv(cell_suspension_data)
420
+ else:
421
+ # Assume CSV or other tabular format
422
+ cell_df = pd.read_csv(cell_suspension_data)
423
+ log += f"Loaded data file containing {len(cell_df)} cells\n"
424
+ else:
425
+ # Assume data is already a DataFrame
426
+ cell_df = cell_suspension_data.copy()
427
+ log += f"Using provided DataFrame containing {len(cell_df)} cells\n"
428
+
429
+ # Validate fluorescence parameter exists in data
430
+ if fluorescence_parameter not in cell_df.columns:
431
+ raise ValueError(f"Fluorescence parameter '{fluorescence_parameter}' not found in data")
432
+
433
+ # Apply gating strategy based on fluorescence thresholds
434
+ log += f"\n## Applying Gating Strategy for {fluorescence_parameter}\n"
435
+ original_count = len(cell_df)
436
+
437
+ # Filter cells based on min/max thresholds
438
+ if threshold_min is not None:
439
+ cell_df = cell_df[cell_df[fluorescence_parameter] >= threshold_min]
440
+ log += f"Applied minimum threshold of {threshold_min}: {len(cell_df)} cells remaining\n"
441
+
442
+ if threshold_max is not None:
443
+ cell_df = cell_df[cell_df[fluorescence_parameter] <= threshold_max]
444
+ log += f"Applied maximum threshold of {threshold_max}: {len(cell_df)} cells remaining\n"
445
+
446
+ # Calculate enrichment statistics
447
+ enrichment_percent = (len(cell_df) / original_count) * 100 if original_count > 0 else 0
448
+ log += "\n## Enrichment Results\n"
449
+ log += f"Original population: {original_count} cells\n"
450
+ log += f"Sorted population: {len(cell_df)} cells\n"
451
+ log += f"Enrichment percentage: {enrichment_percent:.2f}%\n"
452
+
453
+ # Calculate statistics of sorted population
454
+ mean_fluorescence = cell_df[fluorescence_parameter].mean()
455
+ median_fluorescence = cell_df[fluorescence_parameter].median()
456
+
457
+ log += f"Mean {fluorescence_parameter}: {mean_fluorescence:.2f}\n"
458
+ log += f"Median {fluorescence_parameter}: {median_fluorescence:.2f}\n"
459
+
460
+ # Save sorted population to file
461
+ cell_df.to_csv(output_file, index=False)
462
+ log += "\n## Output\n"
463
+ log += f"Sorted cell population saved to: {os.path.abspath(output_file)}\n"
464
+
465
+ return log
466
+
467
+ except Exception as e:
468
+ log += "\n## Error\n"
469
+ log += f"An error occurred during cell sorting: {str(e)}\n"
470
+ return log
471
+
472
+
473
+ def analyze_flow_cytometry_immunophenotyping(
474
+ fcs_file_path, gating_strategy, compensation_matrix=None, output_dir="./results"
475
+ ):
476
+ """Analyze flow cytometry data to identify and quantify specific cell populations based on surface markers.
477
+
478
+ Parameters
479
+ ----------
480
+ fcs_file_path : str
481
+ Path to the FCS file containing flow cytometry data
482
+ gating_strategy : dict
483
+ Dictionary defining the gating strategy. Each key is a population name, and each value is a list of tuples
484
+ (marker, operator, threshold). For example: {'HSCs': [('Lin', '<', 100), ('Sca1', '>', 1000), ...]}
485
+ compensation_matrix : numpy.ndarray, optional
486
+ Spillover/compensation matrix to correct for fluorescence overlap
487
+ output_dir : str, optional
488
+ Directory to save the results
489
+
490
+ Returns
491
+ -------
492
+ str
493
+ Research log summarizing the analysis steps and results
494
+
495
+ """
496
+ import os
497
+
498
+ import pandas as pd
499
+ from FlowCytometryTools import FCMeasurement
500
+
501
+ # Create output directory if it doesn't exist
502
+ if not os.path.exists(output_dir):
503
+ os.makedirs(output_dir)
504
+
505
+ # Load the FCS file
506
+ sample = FCMeasurement(ID="Sample", datafile=fcs_file_path)
507
+
508
+ # Apply compensation if provided
509
+ if compensation_matrix is not None:
510
+ sample = sample.compensate(compensation_matrix)
511
+
512
+ # Initialize log
513
+ log = "Flow Cytometry Analysis Log\n"
514
+ log += "==========================\n"
515
+ log += f"File analyzed: {os.path.basename(fcs_file_path)}\n"
516
+ log += f"Total events: {len(sample)}\n\n"
517
+
518
+ # Create a dictionary to store population counts
519
+ populations = {}
520
+
521
+ # Apply gating strategy to identify cell populations
522
+ for pop_name, gates in gating_strategy.items():
523
+ # Start with all events
524
+ current_population = sample.copy()
525
+
526
+ log += f"Identifying {pop_name} population:\n"
527
+
528
+ # Apply each gate sequentially
529
+ for marker, operator, threshold in gates:
530
+ initial_count = len(current_population)
531
+
532
+ if operator == ">":
533
+ current_population = current_population.gate(f"{marker} > {threshold}")
534
+ elif operator == "<":
535
+ current_population = current_population.gate(f"{marker} < {threshold}")
536
+ elif operator == "between":
537
+ # For 'between', threshold should be a tuple (lower, upper)
538
+ lower, upper = threshold
539
+ current_population = current_population.gate(f"{marker} > {lower} and {marker} < {upper}")
540
+
541
+ final_count = len(current_population)
542
+ log += f" Gate {marker} {operator} {threshold}: {initial_count} → {final_count} events\n"
543
+
544
+ # Store the final population
545
+ populations[pop_name] = current_population
546
+
547
+ # Calculate percentage of original sample
548
+ percentage = (len(current_population) / len(sample)) * 100
549
+ log += f" Final {pop_name} count: {len(current_population)} events ({percentage:.2f}% of total)\n\n"
550
+
551
+ # Create a summary dataframe
552
+ summary_data = {"Population": [], "Count": [], "Percentage": []}
553
+
554
+ for pop_name, population in populations.items():
555
+ summary_data["Population"].append(pop_name)
556
+ summary_data["Count"].append(len(population))
557
+ summary_data["Percentage"].append((len(population) / len(sample)) * 100)
558
+
559
+ summary_df = pd.DataFrame(summary_data)
560
+
561
+ # Save the summary to a CSV file
562
+ summary_file = os.path.join(output_dir, "population_summary.csv")
563
+ summary_df.to_csv(summary_file, index=False)
564
+
565
+ log += "Summary of identified populations:\n"
566
+ for _, row in summary_df.iterrows():
567
+ log += f" {row['Population']}: {row['Count']} events ({row['Percentage']:.2f}%)\n"
568
+
569
+ log += f"\nDetailed results saved to: {summary_file}\n"
570
+
571
+ return log
572
+
573
+
574
+ def analyze_mitochondrial_morphology_and_potential(morphology_image_path, potential_image_path, output_dir="./output"):
575
+ """Quantifies metrics of mitochondrial morphology and membrane potential from fluorescence microscopy images.
576
+
577
+ Parameters
578
+ ----------
579
+ morphology_image_path : str
580
+ Path to the fluorescence microscopy image showing mitochondrial morphology (e.g., MTS-GFP)
581
+ potential_image_path : str
582
+ Path to the fluorescence microscopy image showing mitochondrial membrane potential (e.g., TMRE staining)
583
+ output_dir : str, optional
584
+ Directory to save output files, default is "./output"
585
+
586
+ Returns
587
+ -------
588
+ str
589
+ A research log summarizing the analysis steps and results
590
+
591
+ """
592
+ import datetime
593
+ import os
594
+
595
+ import cv2
596
+ import numpy as np
597
+ from scipy import ndimage
598
+ from skimage import filters, io, morphology, util
599
+
600
+ # Create output directory if it doesn't exist
601
+ os.makedirs(output_dir, exist_ok=True)
602
+
603
+ log = []
604
+ log.append(f"Mitochondrial Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
605
+ log.append("=" * 50)
606
+
607
+ # Load images
608
+ try:
609
+ morph_img = io.imread(morphology_image_path)
610
+ pot_img = io.imread(potential_image_path)
611
+
612
+ log.append(f"Successfully loaded morphology image: {morphology_image_path}")
613
+ log.append(f"Successfully loaded potential image: {potential_image_path}")
614
+ log.append(f"Morphology image shape: {morph_img.shape}")
615
+ log.append(f"Potential image shape: {pot_img.shape}")
616
+ except Exception as e:
617
+ return f"Error loading images: {str(e)}"
618
+
619
+ # Convert to grayscale if needed
620
+ if len(morph_img.shape) > 2:
621
+ morph_img = cv2.cvtColor(morph_img, cv2.COLOR_RGB2GRAY)
622
+ log.append("Converted morphology image to grayscale")
623
+ if len(pot_img.shape) > 2:
624
+ pot_img = cv2.cvtColor(pot_img, cv2.COLOR_RGB2GRAY)
625
+ log.append("Converted potential image to grayscale")
626
+
627
+ # Normalize images
628
+ morph_img = util.img_as_float(morph_img)
629
+ pot_img = util.img_as_float(pot_img)
630
+
631
+ log.append("\nMorphology Analysis:")
632
+ log.append("-" * 30)
633
+
634
+ # Denoise morphology image
635
+ morph_img_denoised = filters.gaussian(morph_img, sigma=1.0)
636
+
637
+ # Threshold to segment mitochondria
638
+ threshold_value = filters.threshold_otsu(morph_img_denoised)
639
+ binary_img = morph_img_denoised > threshold_value
640
+
641
+ # Remove small objects
642
+ binary_img = morphology.remove_small_objects(binary_img, min_size=20)
643
+
644
+ # Save binary image
645
+ binary_path = os.path.join(output_dir, "binary_mitochondria.png")
646
+ io.imsave(binary_path, util.img_as_ubyte(binary_img))
647
+ log.append(f"Binary segmentation saved to: {binary_path}")
648
+
649
+ # Skeletonize for network analysis
650
+ skeleton = morphology.skeletonize(binary_img)
651
+ skeleton_path = os.path.join(output_dir, "skeleton.png")
652
+ io.imsave(skeleton_path, util.img_as_ubyte(skeleton))
653
+ log.append(f"Skeleton image saved to: {skeleton_path}")
654
+
655
+ # Calculate morphology metrics
656
+ # 1. Count branches and junctions
657
+ # Label the skeleton
658
+ labeled_skeleton, num_branches = ndimage.label(skeleton)
659
+
660
+ # Find branch points (pixels with more than 2 neighbors)
661
+ kernel = np.ones((3, 3), dtype=np.uint8)
662
+ kernel[1, 1] = 0 # Don't count the center pixel
663
+ neighbor_count = ndimage.convolve(skeleton.astype(np.uint8), kernel)
664
+ junction_points = (neighbor_count > 2) & skeleton
665
+ num_junctions = np.sum(junction_points)
666
+
667
+ # 2. Calculate fragmentation metrics
668
+ labeled_objects, num_objects = ndimage.label(binary_img)
669
+ object_sizes = ndimage.sum(binary_img, labeled_objects, range(1, num_objects + 1))
670
+ mean_size = np.mean(object_sizes) if len(object_sizes) > 0 else 0
671
+
672
+ # Calculate network connectivity (ratio of junctions to branches)
673
+ connectivity = num_junctions / num_branches if num_branches > 0 else 0
674
+
675
+ # Calculate fragmentation index (inverse of mean object size, normalized)
676
+ fragmentation = 1 / (mean_size / np.max(object_sizes)) if mean_size > 0 and np.max(object_sizes) > 0 else 0
677
+
678
+ log.append(f"Number of mitochondrial fragments: {num_objects}")
679
+ log.append(f"Number of branches: {num_branches}")
680
+ log.append(f"Number of junction points: {num_junctions}")
681
+ log.append(f"Network connectivity index: {connectivity:.4f}")
682
+ log.append(f"Fragmentation index: {fragmentation:.4f}")
683
+ log.append(f"Mean fragment size: {mean_size:.2f} pixels")
684
+
685
+ # Membrane potential analysis
686
+ log.append("\nMembrane Potential Analysis:")
687
+ log.append("-" * 30)
688
+
689
+ # Create mask from morphology image to analyze only mitochondrial regions
690
+ mito_mask = binary_img
691
+
692
+ # Calculate potential metrics within mitochondrial regions
693
+ pot_intensity_raw = np.mean(pot_img)
694
+ pot_intensity_in_mito = np.mean(pot_img[mito_mask]) if np.sum(mito_mask) > 0 else 0
695
+
696
+ # Calculate potential heterogeneity (standard deviation of intensity)
697
+ pot_heterogeneity = np.std(pot_img[mito_mask]) if np.sum(mito_mask) > 0 else 0
698
+
699
+ # Calculate potential distribution
700
+ if np.sum(mito_mask) > 0:
701
+ percentiles = [10, 25, 50, 75, 90]
702
+ pot_percentiles = np.percentile(pot_img[mito_mask], percentiles)
703
+ percentile_str = ", ".join([f"{p}th: {v:.4f}" for p, v in zip(percentiles, pot_percentiles, strict=False)])
704
+ else:
705
+ percentile_str = "No mitochondrial regions detected"
706
+
707
+ log.append(f"Overall mean TMRE intensity: {pot_intensity_raw:.4f}")
708
+ log.append(f"Mean TMRE intensity in mitochondria: {pot_intensity_in_mito:.4f}")
709
+ log.append(f"TMRE intensity heterogeneity (std): {pot_heterogeneity:.4f}")
710
+ log.append(f"TMRE intensity percentiles: {percentile_str}")
711
+
712
+ # Create a combined results image
713
+ # Overlay potential intensity on morphology
714
+ overlay = np.zeros((morph_img.shape[0], morph_img.shape[1], 3), dtype=np.float32)
715
+ overlay[..., 0] = morph_img # Red channel - morphology
716
+ overlay[..., 2] = pot_img # Blue channel - potential
717
+ overlay = np.clip(overlay, 0, 1)
718
+
719
+ overlay_path = os.path.join(output_dir, "morphology_potential_overlay.png")
720
+ io.imsave(overlay_path, util.img_as_ubyte(overlay))
721
+ log.append(f"Overlay image saved to: {overlay_path}")
722
+
723
+ # Save quantitative results to CSV
724
+ import csv
725
+
726
+ results_path = os.path.join(output_dir, "mitochondrial_analysis_results.csv")
727
+ with open(results_path, "w", newline="") as csvfile:
728
+ writer = csv.writer(csvfile)
729
+ writer.writerow(["Metric", "Value"])
730
+ writer.writerow(["Number of fragments", num_objects])
731
+ writer.writerow(["Number of branches", num_branches])
732
+ writer.writerow(["Number of junctions", num_junctions])
733
+ writer.writerow(["Network connectivity", connectivity])
734
+ writer.writerow(["Fragmentation index", fragmentation])
735
+ writer.writerow(["Mean fragment size", mean_size])
736
+ writer.writerow(["Mean TMRE intensity", pot_intensity_in_mito])
737
+ writer.writerow(["TMRE heterogeneity", pot_heterogeneity])
738
+
739
+ log.append(f"Quantitative results saved to: {results_path}")
740
+ log.append("\nAnalysis complete.")
741
+
742
+ return "\n".join(log)
Biomni/biomni/tool/database.py ADDED
The diff for this file is too large to render. See raw diff
 
Biomni/biomni/tool/example_mcp_tools/pubmed_mcp.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from Bio import Entrez
4
+ from mcp.server.fastmcp import FastMCP
5
+ from pydantic import BaseModel
6
+
7
+ Entrez.email = os.getenv("NCBI_EMAIL") or "YOUR_EMAIL"
8
+ mcp = FastMCP("pubmed-live")
9
+
10
+
11
+ class Article(BaseModel):
12
+ pmid: str
13
+ title: str | None = None
14
+ journal: str | None = None
15
+ year: str | None = None
16
+ abstract: str | None = None
17
+
18
+
19
+ @mcp.tool()
20
+ def search_pubmed(query: str, max_results: int = 10) -> list[Article]:
21
+ ids = Entrez.read(Entrez.esearch(db="pubmed", term=query, retmax=max_results))["IdList"]
22
+ summaries = Entrez.read(Entrez.esummary(db="pubmed", id=",".join(ids))) if ids else []
23
+ articles: list[Article] = []
24
+ for s in summaries:
25
+ articles.append(
26
+ Article(pmid=s["Id"], title=s["Title"], journal=s.get("FullJournalName", ""), year=s.get("PubDate", "")[:4])
27
+ )
28
+ return articles
29
+
30
+
31
+ @mcp.tool()
32
+ def get_article_abstract(pmid: str) -> str:
33
+ with Entrez.efetch(db="pubmed", id=pmid, rettype="abstract", retmode="text") as h:
34
+ return h.read()
35
+
36
+
37
+ if __name__ == "__main__":
38
+ mcp.run()
Biomni/biomni/tool/genetics.py ADDED
@@ -0,0 +1,1672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def liftover_coordinates(
2
+ chromosome: str,
3
+ position: int,
4
+ input_format: str,
5
+ output_format: str,
6
+ data_path: str,
7
+ ) -> str:
8
+ """Perform liftover of genomic coordinates between hg19 and hg38 formats with detailed intermediate steps.
9
+
10
+ Args:
11
+ chromosome (str): Chromosome number (e.g., '1', 'X').
12
+ position (int): Genomic position.
13
+ input_format (str): Input genome build ('hg19' or 'hg38').
14
+ output_format (str): Output genome build ('hg19' or 'hg38').
15
+ data_path (str): Path to liftover chain files.
16
+
17
+ Returns:
18
+ str: A detailed string explaining the steps and the final result or any error encountered.
19
+
20
+ """
21
+ from pyliftover import LiftOver
22
+
23
+ steps = []
24
+
25
+ try:
26
+ steps.append(
27
+ f"Starting liftover process for chromosome {chromosome}, position {position} from {input_format} to {output_format}."
28
+ )
29
+
30
+ # Load the liftover chain files
31
+ steps.append("Loading liftover chain files...")
32
+ hg19_to_hg38_liftover = LiftOver(data_path + "/liftover/hg19ToHg38.over.chain.gz")
33
+ hg38_to_hg19_liftover = LiftOver(data_path + "/liftover/hg38ToHg19.over.chain.gz")
34
+ steps.append("Liftover chain files loaded successfully.")
35
+
36
+ # Choose the appropriate LiftOver object
37
+ if input_format == "hg19" and output_format == "hg38":
38
+ lo = hg19_to_hg38_liftover
39
+ steps.append("Selected liftover chain: hg19 to hg38.")
40
+ elif input_format == "hg38" and output_format == "hg19":
41
+ lo = hg38_to_hg19_liftover
42
+ steps.append("Selected liftover chain: hg38 to hg19.")
43
+ else:
44
+ steps.append("Error: Unsupported format conversion.")
45
+ return "\n".join(
46
+ steps
47
+ + ["Error: Unsupported format conversion. Supported formats are 'hg19' to 'hg38' or 'hg38' to 'hg19'."]
48
+ )
49
+
50
+ # Perform the liftover conversion
51
+ steps.append(f"Performing liftover for chr{chromosome}, position {position}...")
52
+ lifted_coordinates = lo.convert_coordinate(f"chr{chromosome}", position)
53
+
54
+ if lifted_coordinates:
55
+ result = (
56
+ f"Successfully lifted coordinates from {input_format} to {output_format}.\n"
57
+ f"Original: chr{chromosome}, position {position}.\n"
58
+ f"Lifted: chromosome {lifted_coordinates[0][0]}, position {lifted_coordinates[0][1]}, strand {lifted_coordinates[0][2]}."
59
+ )
60
+ steps.append(result)
61
+ return "\n".join(steps)
62
+ else:
63
+ steps.append("Error: Liftover failed. No coordinates found for the given input.")
64
+ return "\n".join(steps + ["Error: Liftover failed. No coordinates found."])
65
+
66
+ except Exception as e:
67
+ steps.append(f"Exception encountered: {str(e)}")
68
+ return "\n".join(steps)
69
+
70
+
71
+ import os
72
+ from datetime import datetime
73
+
74
+ import numpy as np
75
+ import pandas as pd
76
+ import torch
77
+ from torch import nn, optim
78
+
79
+
80
+ def bayesian_finemapping_with_deep_vi(
81
+ gwas_summary_path,
82
+ ld_matrix,
83
+ n_iterations=5000,
84
+ learning_rate=0.01,
85
+ hidden_dim=64,
86
+ credible_threshold=0.95,
87
+ ):
88
+ """Performs Bayesian fine-mapping from GWAS summary statistics using deep variational inference.
89
+
90
+ This function implements a deep neural network-based variational inference approach to compute
91
+ posterior inclusion probabilities (PIPs) and credible sets for putative causal variants from
92
+ GWAS summary statistics and linkage disequilibrium (LD) information.
93
+
94
+ Parameters
95
+ ----------
96
+ gwas_summary_path : str
97
+ Path to CSV or TSV file containing GWAS summary statistics. Expected columns:
98
+ - 'variant_id': Identifier for each variant
99
+ - 'effect_size': Effect size (beta) for each variant
100
+ - 'pvalue': P-value for each variant
101
+ - 'se': Standard error for each variant (optional)
102
+
103
+ ld_matrix : numpy.ndarray
104
+ Linkage disequilibrium matrix with pairwise correlations between variants.
105
+
106
+ n_iterations : int, optional
107
+ Number of training iterations for the variational inference algorithm.
108
+ Default is 5000.
109
+
110
+ learning_rate : float, optional
111
+ Learning rate for the optimization algorithm. Default is 0.01.
112
+
113
+ hidden_dim : int, optional
114
+ Hidden dimension size for the neural network. Default is 64.
115
+
116
+ credible_threshold : float, optional
117
+ Threshold for defining the credible set (e.g., 0.95 for a 95% credible set).
118
+ Default is 0.95.
119
+
120
+ Returns
121
+ -------
122
+ str
123
+ A detailed research log of the fine-mapping analysis including:
124
+ - Number of variants analyzed
125
+ - Top variants ranked by posterior inclusion probability
126
+ - Credible set variants
127
+ - Visualizations of the posterior distributions
128
+
129
+ """
130
+ import matplotlib.pyplot as plt
131
+ import pandas as pd
132
+
133
+ # Initialize the research log
134
+ log = []
135
+ log.append(
136
+ f"# Bayesian Fine-mapping Analysis with Deep Variational Inference - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
137
+ )
138
+ log.append("\n## Data Preprocessing")
139
+
140
+ # Load data from file
141
+ try:
142
+ if gwas_summary_path.endswith(".csv"):
143
+ gwas_summary = pd.read_csv(gwas_summary_path)
144
+ elif gwas_summary_path.endswith((".tsv", ".txt")):
145
+ gwas_summary = pd.read_csv(gwas_summary_path, sep="\t")
146
+ else:
147
+ log.append("Error: Unsupported file format. Please provide a CSV or TSV file.")
148
+ return "\n".join(log)
149
+ log.append(f"Successfully loaded GWAS summary data from {gwas_summary_path}")
150
+ except Exception as e:
151
+ log.append(f"Error loading GWAS summary data: {str(e)}")
152
+ return "\n".join(log)
153
+
154
+ # Check input data
155
+ if gwas_summary is None:
156
+ log.append("Error: Failed to load GWAS summary data.")
157
+ return "\n".join(log)
158
+
159
+ if ld_matrix is None:
160
+ log.append("Error: LD matrix is required for fine-mapping analysis.")
161
+ return "\n".join(log)
162
+
163
+ n_variants = len(gwas_summary)
164
+ log.append(f"Analyzing {n_variants} genetic variants")
165
+
166
+ # Check if LD matrix dimensions match the number of variants
167
+ if ld_matrix.shape[0] != n_variants or ld_matrix.shape[1] != n_variants:
168
+ log.append(f"Error: LD matrix dimensions ({ld_matrix.shape}) do not match number of variants ({n_variants})")
169
+ return "\n".join(log)
170
+
171
+ # Prepare data for analysis
172
+ log.append("\nPreparing data for analysis...")
173
+
174
+ # Compute Z-scores if not already present
175
+ if "z_score" not in gwas_summary.columns:
176
+ log.append("Computing Z-scores from effect sizes and standard errors...")
177
+ if "se" in gwas_summary.columns:
178
+ gwas_summary["z_score"] = gwas_summary["effect_size"] / gwas_summary["se"]
179
+ else:
180
+ # Approximate Z-scores from p-values
181
+ log.append("Standard errors not available, approximating Z-scores from p-values...")
182
+ # Convert p-values to Z-scores (two-sided test)
183
+ from scipy.stats import norm
184
+
185
+ gwas_summary["z_score"] = (
186
+ gwas_summary["effect_size"].abs()
187
+ / gwas_summary["effect_size"]
188
+ * norm.ppf(1 - gwas_summary["pvalue"] / 2)
189
+ )
190
+
191
+ # Convert data to tensors
192
+ z_scores = torch.FloatTensor(gwas_summary["z_score"].values)
193
+ ld_tensor = torch.FloatTensor(ld_matrix)
194
+
195
+ log.append(f"Processed {len(z_scores)} z-scores from GWAS summary")
196
+ log.append("LD matrix shape: " + str(ld_matrix.shape))
197
+
198
+ # Define the variational inference model
199
+ class VariationalFineMapping(nn.Module):
200
+ def __init__(self, n_variants, hidden_dim):
201
+ super().__init__()
202
+ self.encoder = nn.Sequential(
203
+ nn.Linear(n_variants, hidden_dim),
204
+ nn.ReLU(),
205
+ nn.Linear(hidden_dim, hidden_dim),
206
+ nn.ReLU(),
207
+ )
208
+ # Output log alpha parameters for the Bernoulli variables
209
+ self.log_alpha = nn.Linear(hidden_dim, n_variants)
210
+
211
+ def forward(self, x):
212
+ h = self.encoder(x)
213
+ log_alpha = self.log_alpha(h)
214
+ # Apply sigmoid to get inclusion probabilities
215
+ return torch.sigmoid(log_alpha)
216
+
217
+ def elbo_loss(self, z_scores, ld_matrix, pips, n_samples=10):
218
+ # Sample from approximate posterior
219
+ samples = torch.bernoulli(pips.unsqueeze(0).repeat(n_samples, 1))
220
+
221
+ # Prior term (sparsity prior)
222
+ prior_term = -0.01 * torch.sum(pips)
223
+
224
+ # Likelihood term
225
+ likelihood_term = 0
226
+ for s in samples:
227
+ # Compute expected z-scores under the model
228
+ expected_z = torch.matmul(ld_matrix, s * z_scores)
229
+ # Compute likelihood
230
+ likelihood_term += -torch.sum((z_scores - expected_z) ** 2)
231
+
232
+ likelihood_term /= n_samples
233
+
234
+ return -(prior_term + likelihood_term)
235
+
236
+ # Initialize model, optimizer and training
237
+ log.append("\n## Initializing deep variational inference model")
238
+ model = VariationalFineMapping(n_variants, hidden_dim)
239
+ optimizer = optim.Adam(model.parameters(), lr=learning_rate)
240
+
241
+ # Training loop
242
+ log.append("\n## Training variational inference model")
243
+ losses = []
244
+
245
+ for i in range(n_iterations):
246
+ optimizer.zero_grad()
247
+ pips = model(z_scores)
248
+ loss = model.elbo_loss(z_scores, ld_tensor, pips)
249
+ loss.backward()
250
+ optimizer.step()
251
+
252
+ losses.append(loss.item())
253
+
254
+ if (i + 1) % (n_iterations // 5) == 0:
255
+ log.append(f" Iteration {i + 1}/{n_iterations}, Loss: {loss.item():.4f}")
256
+
257
+ # Get final posterior inclusion probabilities
258
+ with torch.no_grad():
259
+ final_pips = model(z_scores).numpy()
260
+
261
+ # Create DataFrame with results
262
+ results_df = gwas_summary.copy()
263
+ results_df["pip"] = final_pips
264
+ results_df = results_df.sort_values("pip", ascending=False)
265
+
266
+ # Generate credible sets
267
+ log.append("\n## Generating credible sets")
268
+
269
+ # Sort variants by PIP
270
+ sorted_variants = results_df.sort_values("pip", ascending=False)
271
+
272
+ # Calculate cumulative sum of PIPs
273
+ sorted_variants["cumulative_pip"] = sorted_variants["pip"].cumsum()
274
+
275
+ # Identify variants in the credible set
276
+ credible_set = sorted_variants[sorted_variants["cumulative_pip"] <= credible_threshold]
277
+
278
+ if len(credible_set) == 0:
279
+ # If no variants meet the threshold, include at least the top variant
280
+ credible_set = sorted_variants.iloc[:1]
281
+
282
+ log.append(f"Identified {len(credible_set)} variants in the {credible_threshold * 100}% credible set")
283
+
284
+ # Save results to files
285
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
286
+ results_file = f"finemapping_results_{timestamp}.csv"
287
+ credible_set_file = f"credible_set_{timestamp}.csv"
288
+
289
+ results_df.to_csv(results_file, index=False)
290
+ credible_set.to_csv(credible_set_file, index=False)
291
+
292
+ log.append(f"\nFull results saved to: {results_file}")
293
+ log.append(f"Credible set saved to: {credible_set_file}")
294
+
295
+ # Summary of top variants
296
+ log.append("\n## Top variants by posterior inclusion probability (PIP)")
297
+ for i, (_, row) in enumerate(results_df.head(10).iterrows()):
298
+ log.append(f" {i + 1}. Variant: {row['variant_id']}, PIP: {row['pip']:.4f}, P-value: {row['pvalue']:.2e}")
299
+
300
+ log.append("\n## Variants in the credible set")
301
+ for i, (_, row) in enumerate(credible_set.iterrows()):
302
+ log.append(f" {i + 1}. Variant: {row['variant_id']}, PIP: {row['pip']:.4f}")
303
+
304
+ # Create a simple visualization
305
+ try:
306
+ plt.figure(figsize=(10, 6))
307
+ plt.bar(range(len(results_df[:50])), results_df["pip"][:50])
308
+ plt.xlabel("Variant index (sorted by PIP)")
309
+ plt.ylabel("Posterior Inclusion Probability")
310
+ plt.title("Top 50 variants by PIP")
311
+ plot_file = f"pip_plot_{timestamp}.png"
312
+ plt.savefig(plot_file)
313
+ plt.close()
314
+ log.append(f"\nPlot of PIPs saved to: {plot_file}")
315
+ except Exception as e:
316
+ log.append(f"\nCould not create visualization: {str(e)}")
317
+
318
+ log.append("\n## Analysis complete")
319
+
320
+ return "\n".join(log)
321
+
322
+
323
+ def analyze_cas9_mutation_outcomes(
324
+ reference_sequences,
325
+ edited_sequences,
326
+ cell_line_info=None,
327
+ output_prefix="cas9_mutation_analysis",
328
+ ):
329
+ """Analyzes and categorizes mutations induced by Cas9 at target sites.
330
+
331
+ Parameters
332
+ ----------
333
+ reference_sequences : dict
334
+ Dictionary mapping sequence IDs to reference DNA sequences (strings)
335
+ edited_sequences : dict of dict
336
+ Nested dictionary: {sequence_id: {read_id: sequence}}
337
+ Contains the edited/mutated sequences for each reference
338
+ cell_line_info : dict, optional
339
+ Dictionary mapping sequence IDs to cell line information (e.g., wildtype, knockout gene)
340
+ output_prefix : str, optional
341
+ Prefix for output files
342
+
343
+ Returns
344
+ -------
345
+ str
346
+ Research log summarizing the analysis steps and results
347
+
348
+ """
349
+ from collections import defaultdict
350
+
351
+ from Bio import pairwise2
352
+
353
+ # Initialize results storage
354
+ results = []
355
+ mutation_counts = defaultdict(lambda: defaultdict(int))
356
+
357
+ # Define mutation categories
358
+ categories = {
359
+ "no_mutation": "No mutation detected",
360
+ "short_deletion": "Short deletion (1-10 bp)",
361
+ "medium_deletion": "Medium deletion (11-30 bp)",
362
+ "long_deletion": "Long deletion (>30 bp)",
363
+ "single_insertion": "Single base insertion",
364
+ "longer_insertion": "Longer insertion (>1 bp)",
365
+ "indel": "Insertion and deletion",
366
+ }
367
+
368
+ log = "# Cas9-Induced Mutation Outcome Analysis\n\n"
369
+ log += "## Analysis Steps:\n\n"
370
+ log += "1. Loading and processing sequence data\n"
371
+ log += f"2. Analyzing {len(reference_sequences)} target sites\n"
372
+
373
+ # Process each reference sequence and its edited versions
374
+ for seq_id, ref_seq in reference_sequences.items():
375
+ cell_line = cell_line_info.get(seq_id, "Unknown") if cell_line_info else "Unknown"
376
+ log += f"\n### Processing target site: {seq_id} (Cell line: {cell_line})\n"
377
+
378
+ site_results = []
379
+ site_mutation_counts = defaultdict(int)
380
+ total_reads = len(edited_sequences.get(seq_id, {}))
381
+
382
+ if total_reads == 0:
383
+ log += f"No edited sequences found for {seq_id}\n"
384
+ continue
385
+
386
+ log += f"Analyzing {total_reads} sequence reads...\n"
387
+
388
+ # Process each edited sequence for this reference
389
+ for read_id, edited_seq in edited_sequences.get(seq_id, {}).items():
390
+ # Perform sequence alignment
391
+ alignments = pairwise2.align.globalms(ref_seq, edited_seq, 2, -1, -2, -0.5)
392
+
393
+ if not alignments:
394
+ log += f"Warning: Could not align read {read_id}\n"
395
+ continue
396
+
397
+ best_alignment = alignments[0]
398
+ ref_aligned, edited_aligned, score, start, end = best_alignment
399
+
400
+ # Analyze mutations
401
+ deletions = []
402
+ insertions = []
403
+ del_count = 0
404
+ ins_count = 0
405
+
406
+ i, j = 0, 0
407
+ while i < len(ref_aligned) and j < len(edited_aligned):
408
+ if ref_aligned[i] == "-": # Insertion in edited sequence
409
+ ins_start = j
410
+ while i < len(ref_aligned) and ref_aligned[i] == "-":
411
+ i += 1
412
+ j += 1
413
+ insertions.append((ins_start, j - ins_start))
414
+ ins_count += j - ins_start
415
+ elif edited_aligned[j] == "-": # Deletion in edited sequence
416
+ del_start = i
417
+ while j < len(edited_aligned) and edited_aligned[j] == "-":
418
+ i += 1
419
+ j += 1
420
+ deletions.append((del_start, i - del_start))
421
+ del_count += i - del_start
422
+ else:
423
+ i += 1
424
+ j += 1
425
+
426
+ # Categorize mutation
427
+ mutation_type = "no_mutation"
428
+ if del_count > 0 and ins_count > 0:
429
+ mutation_type = "indel"
430
+ elif del_count > 0:
431
+ if del_count <= 10:
432
+ mutation_type = "short_deletion"
433
+ elif del_count <= 30:
434
+ mutation_type = "medium_deletion"
435
+ else:
436
+ mutation_type = "long_deletion"
437
+ elif ins_count > 0:
438
+ mutation_type = "single_insertion" if ins_count == 1 else "longer_insertion"
439
+
440
+ # Add to results
441
+ site_results.append(
442
+ {
443
+ "sequence_id": seq_id,
444
+ "read_id": read_id,
445
+ "cell_line": cell_line,
446
+ "mutation_type": mutation_type,
447
+ "deletion_count": del_count,
448
+ "insertion_count": ins_count,
449
+ }
450
+ )
451
+
452
+ site_mutation_counts[mutation_type] += 1
453
+ mutation_counts[cell_line][mutation_type] += 1
454
+
455
+ # Calculate percentages for this site
456
+ log += "\nMutation distribution for this target site:\n"
457
+ for mut_type, count in site_mutation_counts.items():
458
+ percentage = (count / total_reads) * 100
459
+ log += f"- {categories[mut_type]}: {count} reads ({percentage:.1f}%)\n"
460
+
461
+ # Add site results to overall results
462
+ results.extend(site_results)
463
+
464
+ # Create results dataframe and save to CSV
465
+ results_df = pd.DataFrame(results)
466
+ output_file = f"{output_prefix}_detailed_results.csv"
467
+ results_df.to_csv(output_file, index=False)
468
+
469
+ # Create summary dataframe
470
+ summary_data = []
471
+ for cell_line, mut_counts in mutation_counts.items():
472
+ total = sum(mut_counts.values())
473
+ for mut_type, count in mut_counts.items():
474
+ percentage = (count / total) * 100 if total > 0 else 0
475
+ summary_data.append(
476
+ {
477
+ "cell_line": cell_line,
478
+ "mutation_type": mut_type,
479
+ "count": count,
480
+ "percentage": percentage,
481
+ }
482
+ )
483
+
484
+ summary_df = pd.DataFrame(summary_data)
485
+ summary_file = f"{output_prefix}_summary.csv"
486
+ summary_df.to_csv(summary_file, index=False)
487
+
488
+ # Add summary to log
489
+ log += "\n## Overall Results Summary\n\n"
490
+ log += f"Total sequences analyzed: {len(results)}\n"
491
+ log += f"Detailed results saved to: {output_file}\n"
492
+ log += f"Summary results saved to: {summary_file}\n\n"
493
+
494
+ if cell_line_info:
495
+ log += "### Mutation Distribution by Cell Line\n\n"
496
+ for cell_line, mut_counts in mutation_counts.items():
497
+ total = sum(mut_counts.values())
498
+ if total == 0:
499
+ continue
500
+
501
+ log += f"#### {cell_line}\n"
502
+ for mut_type, count in sorted(mut_counts.items(), key=lambda x: x[1], reverse=True):
503
+ percentage = (count / total) * 100
504
+ log += f"- {categories[mut_type]}: {count} ({percentage:.1f}%)\n"
505
+ log += "\n"
506
+
507
+ return log
508
+
509
+
510
+ def analyze_crispr_genome_editing(original_sequence, edited_sequence, guide_rna, repair_template=None):
511
+ """Analyzes CRISPR-Cas9 genome editing results by comparing original and edited sequences.
512
+
513
+ Parameters
514
+ ----------
515
+ original_sequence : str
516
+ The original DNA sequence before CRISPR-Cas9 editing
517
+ edited_sequence : str
518
+ The DNA sequence after CRISPR-Cas9 editing
519
+ guide_rna : str
520
+ The CRISPR guide RNA (crRNA) sequence used for targeting
521
+ repair_template : str, optional
522
+ The homology-directed repair template sequence, if used
523
+
524
+ Returns
525
+ -------
526
+ str
527
+ A research log summarizing the CRISPR-Cas9 editing analysis, including identified
528
+ mutations and characterization of the edited loci
529
+
530
+ """
531
+ import datetime
532
+
533
+ from Bio import pairwise2
534
+ from Bio.Seq import Seq
535
+
536
+ log = []
537
+ log.append(f"CRISPR-Cas9 Genome Editing Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
538
+ log.append("=" * 70)
539
+
540
+ # Step 1: Find the target site in the original sequence
541
+ log.append("\n1. Identifying target site in original sequence")
542
+ target_site = original_sequence.find(guide_rna)
543
+ if target_site == -1:
544
+ # Try with the reverse complement
545
+ guide_rna_seq = Seq(guide_rna)
546
+ rev_comp_guide = str(guide_rna_seq.reverse_complement())
547
+ target_site = original_sequence.find(rev_comp_guide)
548
+ if target_site != -1:
549
+ log.append(f" - Target site found at position {target_site} (using reverse complement of guide RNA)")
550
+ guide_rna = rev_comp_guide
551
+ else:
552
+ log.append(" - Warning: Guide RNA sequence not found in original sequence")
553
+ target_site = None
554
+ else:
555
+ log.append(f" - Target site found at position {target_site}")
556
+
557
+ # Step 2: Align sequences to identify mutations
558
+ log.append("\n2. Aligning original and edited sequences to identify mutations")
559
+ alignments = pairwise2.align.globalms(original_sequence, edited_sequence, 2, -1, -2, -0.5)
560
+ best_alignment = alignments[0]
561
+
562
+ # Extract aligned sequences
563
+ aligned_orig = best_alignment[0]
564
+ aligned_edit = best_alignment[1]
565
+
566
+ # Find mutations
567
+ mutations = []
568
+ indels = []
569
+
570
+ for i in range(len(aligned_orig)):
571
+ if aligned_orig[i] != aligned_edit[i]:
572
+ orig_base = aligned_orig[i]
573
+ edit_base = aligned_edit[i]
574
+
575
+ # Skip gaps in counting the actual position
576
+ actual_pos = len(aligned_orig[:i].replace("-", ""))
577
+
578
+ if orig_base == "-": # Insertion
579
+ indels.append(f"Insertion of {edit_base} at position {actual_pos}")
580
+ elif edit_base == "-": # Deletion
581
+ indels.append(f"Deletion of {orig_base} at position {actual_pos}")
582
+ else: # Substitution
583
+ mutations.append(f"{orig_base}→{edit_base} at position {actual_pos}")
584
+
585
+ # Log mutations
586
+ if mutations:
587
+ log.append(" - Substitutions detected:")
588
+ for mutation in mutations:
589
+ log.append(f" * {mutation}")
590
+ else:
591
+ log.append(" - No substitutions detected")
592
+
593
+ if indels:
594
+ log.append(" - Insertions/Deletions detected:")
595
+ for indel in indels:
596
+ log.append(f" * {indel}")
597
+ else:
598
+ log.append(" - No insertions or deletions detected")
599
+
600
+ # Step 3: Check if editing occurred near the target site
601
+ if target_site is not None:
602
+ log.append("\n3. Analyzing mutations relative to target site")
603
+ target_end = target_site + len(guide_rna)
604
+ target_region = range(target_site - 3, target_end + 3) # Include some buffer
605
+
606
+ on_target_edits = [m for m in mutations + indels if any(str(pos) in m for pos in target_region)]
607
+
608
+ if on_target_edits:
609
+ log.append(" - On-target edits detected near guide RNA binding site:")
610
+ for edit in on_target_edits:
611
+ log.append(f" * {edit}")
612
+ else:
613
+ log.append(" - No edits detected near guide RNA binding site")
614
+
615
+ # Step 4: Check for homology-directed repair if template was provided
616
+ if repair_template:
617
+ log.append("\n4. Checking for homology-directed repair template incorporation")
618
+ # Look for unique sequence markers from the repair template
619
+ template_len = len(repair_template)
620
+ marker_size = min(10, template_len // 3)
621
+ marker = repair_template[template_len // 2 - marker_size // 2 : template_len // 2 + marker_size // 2]
622
+
623
+ if marker in edited_sequence and marker not in original_sequence:
624
+ log.append(f" - Repair template marker '{marker}' found in edited sequence")
625
+ log.append(" - Homology-directed repair likely successful")
626
+ else:
627
+ log.append(" - No clear evidence of repair template incorporation")
628
+ log.append(" - Editing likely resulted from non-homologous end joining (NHEJ)")
629
+
630
+ # Step 5: Overall assessment
631
+ log.append("\n5. Overall assessment")
632
+ if mutations or indels:
633
+ log.append(" - CRISPR-Cas9 editing appears successful")
634
+ if target_site is not None and any(
635
+ str(pos) in "".join(mutations + indels) for pos in range(target_site, target_site + len(guide_rna))
636
+ ):
637
+ log.append(" - Edits occurred at the intended target site")
638
+ else:
639
+ log.append(" - Edits may have occurred outside the intended target site")
640
+ else:
641
+ log.append(" - No significant editing detected, CRISPR-Cas9 may not have been effective")
642
+
643
+ return "\n".join(log)
644
+
645
+
646
+ def simulate_demographic_history(
647
+ num_samples=10,
648
+ sequence_length=100000,
649
+ recombination_rate=1e-8,
650
+ mutation_rate=1e-8,
651
+ demographic_model="constant",
652
+ demographic_params=None,
653
+ coalescent_model="kingman",
654
+ beta_coalescent_param=None,
655
+ random_seed=None,
656
+ output_file="simulated_sequences.vcf",
657
+ ):
658
+ """Simulate DNA sequences with specified demographic and coalescent histories using msprime.
659
+
660
+ Parameters
661
+ ----------
662
+ num_samples : int
663
+ Number of sample sequences to simulate
664
+ sequence_length : int
665
+ Length of the simulated sequence in base pairs
666
+ recombination_rate : float
667
+ Per-base recombination rate
668
+ mutation_rate : float
669
+ Per-base mutation rate
670
+ demographic_model : str
671
+ Type of demographic model to simulate. Options:
672
+ - "constant": Constant population size
673
+ - "bottleneck": Population bottleneck
674
+ - "expansion": Population expansion
675
+ - "contraction": Population contraction
676
+ - "sawtooth": Sawtooth pattern of population size changes
677
+ demographic_params : dict
678
+ Parameters specific to the chosen demographic model. Supported formats::
679
+
680
+ - For "constant": {"N": population size}
681
+ - For "bottleneck": {
682
+ "N_initial": initial pop size,
683
+ "N_bottleneck": bottleneck pop size,
684
+ "T_bottleneck": time of bottleneck (generations ago),
685
+ "T_recovery": time of recovery (generations ago)
686
+ }
687
+ - For "expansion": {"N_initial": initial pop size, "N_final": final pop size, "T_expansion": time of expansion (generations ago)}
688
+ - For "contraction": {"N_initial": initial pop size, "N_final": final pop size, "T_contraction": time of contraction (generations ago)}
689
+ - For "sawtooth": {"N_values": list of population sizes, "times": list of times for changes}
690
+ coalescent_model : str
691
+ Type of coalescent model to use. Options:
692
+ - "kingman": Standard Kingman coalescent
693
+ - "beta": Beta-coalescent model
694
+ beta_coalescent_param : float
695
+ Parameter for beta-coalescent model (required if coalescent_model="beta")
696
+ random_seed : int
697
+ Seed for random number generator (for reproducibility)
698
+ output_file : str
699
+ Filename to save the simulated sequences (VCF format)
700
+
701
+ Returns
702
+ -------
703
+ str
704
+ Research log summarizing the simulation parameters and results
705
+
706
+ """
707
+ import time
708
+
709
+ import msprime
710
+
711
+ start_time = time.time()
712
+ log = []
713
+ log.append("Demographic History Simulation using msprime")
714
+ log.append("=============================================")
715
+ log.append("Parameters:")
716
+ log.append(f" - Number of samples: {num_samples}")
717
+ log.append(f" - Sequence length: {sequence_length} bp")
718
+ log.append(f" - Recombination rate: {recombination_rate}")
719
+ log.append(f" - Mutation rate: {mutation_rate}")
720
+ log.append(f" - Demographic model: {demographic_model}")
721
+ log.append(f" - Coalescent model: {coalescent_model}")
722
+
723
+ # Set up demographic model
724
+ if demographic_params is None:
725
+ demographic_params = {"N": 10000} # Default to constant population of 10000
726
+
727
+ demography = msprime.Demography()
728
+ demography.add_population(name="pop0", initial_size=1000) # Default initial population
729
+
730
+ if demographic_model == "constant":
731
+ N = demographic_params.get("N", 10000)
732
+ demography.add_population(name="pop0", initial_size=N)
733
+ log.append(f" - Constant population size: N = {N}")
734
+
735
+ elif demographic_model == "bottleneck":
736
+ N_initial = demographic_params.get("N_initial", 10000)
737
+ N_bottleneck = demographic_params.get("N_bottleneck", 1000)
738
+ T_bottleneck = demographic_params.get("T_bottleneck", 1000)
739
+ T_recovery = demographic_params.get("T_recovery", 500)
740
+
741
+ demography = msprime.Demography()
742
+ demography.add_population(name="pop0", initial_size=N_initial)
743
+ demography.add_population_parameters_change(time=T_recovery, initial_size=N_bottleneck)
744
+ demography.add_population_parameters_change(time=T_bottleneck, initial_size=N_initial)
745
+
746
+ log.append(" - Bottleneck model:")
747
+ log.append(f" * Initial population size: {N_initial}")
748
+ log.append(f" * Bottleneck population size: {N_bottleneck}")
749
+ log.append(f" * Bottleneck time (generations ago): {T_bottleneck}")
750
+ log.append(f" * Recovery time (generations ago): {T_recovery}")
751
+
752
+ elif demographic_model == "expansion":
753
+ N_initial = demographic_params.get("N_initial", 1000)
754
+ N_final = demographic_params.get("N_final", 10000)
755
+ T_expansion = demographic_params.get("T_expansion", 1000)
756
+
757
+ demography = msprime.Demography()
758
+ demography.add_population(name="pop0", initial_size=N_final)
759
+ demography.add_population_parameters_change(time=T_expansion, initial_size=N_initial)
760
+
761
+ log.append(" - Expansion model:")
762
+ log.append(f" * Initial population size: {N_initial}")
763
+ log.append(f" * Final population size: {N_final}")
764
+ log.append(f" * Expansion time (generations ago): {T_expansion}")
765
+
766
+ elif demographic_model == "contraction":
767
+ N_initial = demographic_params.get("N_initial", 10000)
768
+ N_final = demographic_params.get("N_final", 1000)
769
+ T_contraction = demographic_params.get("T_contraction", 1000)
770
+
771
+ demography = msprime.Demography()
772
+ demography.add_population(name="pop0", initial_size=N_final)
773
+ demography.add_population_parameters_change(time=T_contraction, initial_size=N_initial)
774
+
775
+ log.append(" - Contraction model:")
776
+ log.append(f" * Initial population size: {N_initial}")
777
+ log.append(f" * Final population size: {N_final}")
778
+ log.append(f" * Contraction time (generations ago): {T_contraction}")
779
+
780
+ elif demographic_model == "sawtooth":
781
+ N_values = demographic_params.get("N_values", [10000, 5000, 15000, 7500])
782
+ times = demographic_params.get("times", [500, 1000, 1500])
783
+
784
+ if len(N_values) != len(times) + 1:
785
+ raise ValueError("For sawtooth model, N_values should have one more element than times")
786
+
787
+ demography = msprime.Demography()
788
+ demography.add_population(name="pop0", initial_size=N_values[0])
789
+
790
+ for i, time in enumerate(times):
791
+ demography.add_population_parameters_change(time=time, initial_size=N_values[i + 1])
792
+
793
+ log.append(" - Sawtooth model:")
794
+ log.append(f" * Population sizes: {N_values}")
795
+ log.append(f" * Change times (generations ago): {times}")
796
+
797
+ else:
798
+ raise ValueError(f"Unknown demographic model: {demographic_model}")
799
+
800
+ # Set up coalescent model
801
+ model = None
802
+ if coalescent_model == "kingman":
803
+ model = msprime.StandardCoalescent()
804
+ log.append(" - Using standard Kingman coalescent")
805
+ elif coalescent_model == "beta":
806
+ if beta_coalescent_param is None:
807
+ beta_coalescent_param = 1.5
808
+ model = msprime.BetaCoalescent(alpha=beta_coalescent_param)
809
+ log.append(f" - Using Beta-coalescent with alpha = {beta_coalescent_param}")
810
+ else:
811
+ raise ValueError(f"Unknown coalescent model: {coalescent_model}")
812
+
813
+ # Run simulation
814
+ log.append("\nRunning simulation...")
815
+
816
+ ts = msprime.sim_ancestry(
817
+ samples=num_samples,
818
+ recombination_rate=recombination_rate,
819
+ sequence_length=sequence_length,
820
+ demography=demography,
821
+ model=model,
822
+ random_seed=random_seed,
823
+ )
824
+
825
+ # Add mutations
826
+ mts = msprime.sim_mutations(ts, rate=mutation_rate, random_seed=random_seed)
827
+
828
+ # Save to VCF
829
+ with open(output_file, "w") as vcf_file:
830
+ mts.write_vcf(vcf_file)
831
+
832
+ # Calculate some basic statistics
833
+ diversity = mts.diversity()
834
+ num_sites = mts.num_sites
835
+ num_trees = mts.num_trees
836
+
837
+ # Log results
838
+ end_time = time.time()
839
+ runtime = end_time - start_time
840
+
841
+ log.append(f"Simulation completed in {runtime:.2f} seconds")
842
+ log.append("\nResults:")
843
+ log.append(f" - Number of segregating sites: {num_sites}")
844
+ log.append(f" - Number of trees in ARG: {num_trees}")
845
+ log.append(f" - Nucleotide diversity (π): {diversity:.6f}")
846
+ log.append(f" - Output saved to: {os.path.abspath(output_file)}")
847
+
848
+ return "\n".join(log)
849
+
850
+
851
+ def identify_transcription_factor_binding_sites(sequence, tf_name, threshold=0.8, output_file=None):
852
+ """Identifies binding sites for a specific transcription factor in a genomic sequence.
853
+
854
+ Parameters
855
+ ----------
856
+ sequence : str
857
+ The genomic DNA sequence to analyze
858
+ tf_name : str
859
+ Name of the transcription factor to search for (e.g., 'Hsf1', 'GATA1')
860
+ threshold : float, optional
861
+ Minimum score threshold for reporting binding sites (0.0-1.0, default: 0.8)
862
+ output_file : str, optional
863
+ Path to save the results (default: None, results only in log)
864
+
865
+ Returns
866
+ -------
867
+ str
868
+ Research log detailing the binding site identification process and results
869
+
870
+ """
871
+ import datetime
872
+ import io
873
+
874
+ import requests
875
+ from Bio import motifs
876
+ from Bio.Seq import Seq
877
+
878
+ log = f"# Transcription Factor Binding Site Analysis: {tf_name}\n"
879
+ log += f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
880
+
881
+ # Step 1: Get the PWM for the transcription factor from JASPAR database
882
+ log += "## Step 1: Retrieving transcription factor PWM\n"
883
+
884
+ try:
885
+ # Search for the TF in JASPAR database
886
+ jaspar_url = f"https://jaspar.genereg.net/api/v1/matrix/?name={tf_name}"
887
+ response = requests.get(jaspar_url)
888
+ tf_data = response.json()
889
+
890
+ if not tf_data["results"] or len(tf_data["results"]) == 0:
891
+ log += f"No PWM found for {tf_name} in JASPAR database.\n"
892
+ return log
893
+
894
+ # Get the first match's ID
895
+ matrix_id = tf_data["results"][0]["matrix_id"]
896
+ log += f"Found PWM with ID: {matrix_id}\n"
897
+
898
+ # Retrieve the PWM
899
+ pwm_url = f"https://jaspar.genereg.net/api/v1/matrix/{matrix_id}.pfm"
900
+ pwm_response = requests.get(pwm_url)
901
+
902
+ # Parse the PWM
903
+ handle = io.StringIO(pwm_response.text)
904
+ motif = motifs.read(handle, "jaspar")
905
+ log += f"Successfully retrieved PWM for {tf_name}\n"
906
+
907
+ # Calculate position-specific scoring matrix (PSSM)
908
+ pssm = motif.pssm
909
+
910
+ # Step 2: Scan the sequence for binding sites
911
+ log += "\n## Step 2: Scanning sequence for binding sites\n"
912
+ log += f"Sequence length: {len(sequence)} bp\n"
913
+ log += f"Using score threshold: {threshold}\n\n"
914
+
915
+ # Find binding sites
916
+ binding_sites = []
917
+ max_score = pssm.max
918
+ min_score = pssm.min
919
+
920
+ for position, score in pssm.search(Seq(sequence), threshold=threshold):
921
+ relative_score = (score - min_score) / (max_score - min_score)
922
+ if position >= 0:
923
+ strand = "+"
924
+ site_seq = sequence[position : position + len(pssm)]
925
+ else:
926
+ strand = "-"
927
+ site_seq = sequence[len(sequence) + position - len(pssm) : len(sequence) + position]
928
+
929
+ binding_sites.append(
930
+ {
931
+ "position": abs(position),
932
+ "strand": strand,
933
+ "score": score,
934
+ "relative_score": relative_score,
935
+ "sequence": site_seq,
936
+ }
937
+ )
938
+
939
+ # Step 3: Summarize results
940
+ log += f"## Step 3: Results - Found {len(binding_sites)} potential binding sites\n\n"
941
+
942
+ if binding_sites:
943
+ # Sort by position
944
+ binding_sites.sort(key=lambda x: x["position"])
945
+
946
+ # Create a table of results
947
+ log += "| Position | Strand | Sequence | Score | Relative Score |\n"
948
+ log += "|----------|--------|----------|-------|---------------|\n"
949
+
950
+ for site in binding_sites:
951
+ log += f"| {site['position']} | {site['strand']} | {site['sequence']} | {site['score']:.2f} | {site['relative_score']:.2f} |\n"
952
+ else:
953
+ log += "No binding sites found meeting the threshold criteria.\n"
954
+
955
+ # Save results to file if specified
956
+ if output_file:
957
+ with open(output_file, "w") as f:
958
+ f.write(f"# {tf_name} binding sites in sequence\n")
959
+ f.write("Position\tStrand\tSequence\tScore\tRelative Score\n")
960
+
961
+ for site in binding_sites:
962
+ f.write(
963
+ f"{site['position']}\t{site['strand']}\t{site['sequence']}\t{site['score']:.2f}\t{site['relative_score']:.2f}\n"
964
+ )
965
+
966
+ log += f"\nResults saved to file: {output_file}\n"
967
+
968
+ except Exception as e:
969
+ log += f"\n## Error occurred during analysis: {str(e)}\n"
970
+
971
+ log += "\n## Analysis complete\n"
972
+ return log
973
+
974
+
975
+ def fit_genomic_prediction_model(
976
+ genotypes,
977
+ phenotypes,
978
+ fixed_effects=None,
979
+ model_type="additive",
980
+ output_file="genomic_prediction_results.csv",
981
+ ):
982
+ """Fit a linear mixed model for genomic prediction using genotype and phenotype data.
983
+
984
+ Parameters
985
+ ----------
986
+ genotypes : numpy.ndarray
987
+ Matrix of genotype data, with individuals in rows and markers in columns.
988
+ Values are typically coded as 0, 1, 2 for additive models or with specific
989
+ encoding for dominance effects.
990
+ phenotypes : numpy.ndarray
991
+ Vector or matrix of phenotype data, with individuals in rows and traits in columns.
992
+ fixed_effects : numpy.ndarray, optional
993
+ Matrix of fixed effects (e.g., environment, management), with individuals in rows
994
+ and effects in columns.
995
+ model_type : str, optional
996
+ Type of genetic model to fit: "additive" or "additive_dominance".
997
+ output_file : str, optional
998
+ File name to save the results.
999
+
1000
+ Returns
1001
+ -------
1002
+ str
1003
+ Research log summarizing the genomic prediction analysis, including model parameters,
1004
+ variance components, breeding values, and prediction accuracy metrics.
1005
+
1006
+ """
1007
+ import pandas as pd
1008
+ from scipy import linalg
1009
+
1010
+ # Initialize research log
1011
+ log = "# Multi-trait Genomic Prediction Analysis\n\n"
1012
+ log += f"Model type: {model_type}\n"
1013
+
1014
+ # Basic validation
1015
+ n_individuals, n_markers = genotypes.shape
1016
+ n_pheno, n_traits = (phenotypes.shape[0], 1) if phenotypes.ndim == 1 else phenotypes.shape
1017
+
1018
+ if n_individuals != n_pheno:
1019
+ raise ValueError(f"Number of individuals in genotypes ({n_individuals}) and phenotypes ({n_pheno}) don't match")
1020
+
1021
+ log += f"Number of individuals: {n_individuals}\n"
1022
+ log += f"Number of markers: {n_markers}\n"
1023
+ log += f"Number of traits: {n_traits}\n\n"
1024
+
1025
+ # Ensure phenotypes is 2D
1026
+ if phenotypes.ndim == 1:
1027
+ phenotypes = phenotypes.reshape(-1, 1)
1028
+
1029
+ # Center genotypes (common preprocessing step)
1030
+ genotypes_centered = genotypes - np.mean(genotypes, axis=0)
1031
+
1032
+ # Create genomic relationship matrix (G)
1033
+ if model_type == "additive":
1034
+ # Additive genomic relationship matrix
1035
+ G = np.dot(genotypes_centered, genotypes_centered.T) / n_markers
1036
+ log += "Constructed additive genomic relationship matrix (G)\n\n"
1037
+ elif model_type == "additive_dominance":
1038
+ # For additive-dominance model, we need both A (additive) and D (dominance) matrices
1039
+ # Assuming genotypes are coded as {0,1,2} for {aa,Aa,AA}
1040
+ # Create dominance matrix - simple implementation assuming standard coding
1041
+ dom_genotypes = np.zeros_like(genotypes)
1042
+ # Code heterozygotes (1) as 1, homozygotes (0,2) as 0 for dominance effects
1043
+ dom_genotypes[genotypes == 1] = 1
1044
+ dom_genotypes_centered = dom_genotypes - np.mean(dom_genotypes, axis=0)
1045
+
1046
+ # Additive and dominance matrices
1047
+ G_a = np.dot(genotypes_centered, genotypes_centered.T) / n_markers
1048
+ G_d = np.dot(dom_genotypes_centered, dom_genotypes_centered.T) / n_markers
1049
+ log += "Constructed additive (G_a) and dominance (G_d) genomic relationship matrices\n\n"
1050
+ else:
1051
+ raise ValueError(f"Unknown model type: {model_type}")
1052
+
1053
+ # Initialize results storage
1054
+ trait_results = []
1055
+
1056
+ # Fit model for each trait
1057
+ for trait_idx in range(n_traits):
1058
+ trait_phenotypes = phenotypes[:, trait_idx]
1059
+ log += f"## Trait {trait_idx + 1} Analysis\n\n"
1060
+
1061
+ # Handle fixed effects if provided
1062
+ if fixed_effects is not None:
1063
+ # Simple fixed effects adjustment - more complex models would use proper mixed model fitting
1064
+ X = fixed_effects
1065
+ # Fit fixed effects model
1066
+ beta = np.linalg.lstsq(X, trait_phenotypes, rcond=None)[0]
1067
+ # Adjust phenotypes for fixed effects
1068
+ y_adj = trait_phenotypes - X @ beta
1069
+ log += f"Applied adjustment for {X.shape[1]} fixed effects\n"
1070
+ else:
1071
+ y_adj = trait_phenotypes
1072
+ log += "No fixed effects provided\n"
1073
+
1074
+ # Fit mixed model
1075
+ if model_type == "additive":
1076
+ # Simplified REML estimation for variance components
1077
+ # In practice, specialized libraries like pyGWAS, GCTA or R's ASReml would be used
1078
+
1079
+ # Initial variance component estimates
1080
+ var_g_init = np.var(y_adj) * 0.5 # genetic variance
1081
+ var_e_init = np.var(y_adj) * 0.5 # residual variance
1082
+
1083
+ # Simple EM-like algorithm for variance component estimation
1084
+ # (In practice, use dedicated software for proper REML)
1085
+ for _ in range(5): # Few iterations for demonstration
1086
+ # Construct mixed model equations
1087
+ V = var_g_init * G + var_e_init * np.eye(n_individuals)
1088
+ V_inv = linalg.inv(V)
1089
+
1090
+ # Update variance components
1091
+ P = V_inv - V_inv @ np.ones((n_individuals, 1)) @ np.ones((1, n_individuals)) @ V_inv / (
1092
+ np.ones((1, n_individuals)) @ V_inv @ np.ones((n_individuals, 1))
1093
+ )
1094
+ var_g_new = (y_adj.T @ P @ G @ P @ y_adj) / np.trace(P @ G)
1095
+ var_e_new = (y_adj.T @ P @ P @ y_adj) / np.trace(P)
1096
+
1097
+ # Update estimates
1098
+ var_g_init = max(0.01, var_g_new)
1099
+ var_e_init = max(0.01, var_e_new)
1100
+
1101
+ # Final variance components
1102
+ var_g = var_g_init
1103
+ var_e = var_e_init
1104
+
1105
+ # Calculate heritability
1106
+ heritability = var_g / (var_g + var_e)
1107
+
1108
+ # BLUP solutions for breeding values
1109
+ V = var_g * G + var_e * np.eye(n_individuals)
1110
+ V_inv = linalg.inv(V)
1111
+ breeding_values = var_g * G @ V_inv @ y_adj
1112
+
1113
+ # Predicted phenotypes
1114
+ predicted_phenotypes = breeding_values
1115
+
1116
+ # Calculate accuracy
1117
+ accuracy = np.corrcoef(trait_phenotypes, predicted_phenotypes)[0, 1]
1118
+
1119
+ # Log results
1120
+ log += f"Estimated additive genetic variance: {var_g:.4f}\n"
1121
+ log += f"Estimated residual variance: {var_e:.4f}\n"
1122
+ log += f"Estimated heritability: {heritability:.4f}\n"
1123
+ log += f"Prediction accuracy (correlation): {accuracy:.4f}\n\n"
1124
+
1125
+ # Store results
1126
+ trait_result = {
1127
+ "trait": trait_idx + 1,
1128
+ "var_g": var_g,
1129
+ "var_e": var_e,
1130
+ "heritability": heritability,
1131
+ "accuracy": accuracy,
1132
+ "breeding_values": breeding_values,
1133
+ "predicted_phenotypes": predicted_phenotypes,
1134
+ }
1135
+ trait_results.append(trait_result)
1136
+
1137
+ elif model_type == "additive_dominance":
1138
+ # Similar approach but with both additive and dominance effects
1139
+ # Initial variance component estimates
1140
+ var_a_init = np.var(y_adj) * 0.4 # additive variance
1141
+ var_d_init = np.var(y_adj) * 0.1 # dominance variance
1142
+ var_e_init = np.var(y_adj) * 0.5 # residual variance
1143
+
1144
+ # Simple estimation iterations
1145
+ for _ in range(5): # Few iterations for demonstration
1146
+ # Construct mixed model equations
1147
+ V = var_a_init * G_a + var_d_init * G_d + var_e_init * np.eye(n_individuals)
1148
+ V_inv = linalg.inv(V)
1149
+
1150
+ # Update variance components (simplified)
1151
+ P = V_inv - V_inv @ np.ones((n_individuals, 1)) @ np.ones((1, n_individuals)) @ V_inv / (
1152
+ np.ones((1, n_individuals)) @ V_inv @ np.ones((n_individuals, 1))
1153
+ )
1154
+ var_a_new = (y_adj.T @ P @ G_a @ P @ y_adj) / np.trace(P @ G_a)
1155
+ var_d_new = (y_adj.T @ P @ G_d @ P @ y_adj) / np.trace(P @ G_d)
1156
+ var_e_new = (y_adj.T @ P @ P @ y_adj) / np.trace(P)
1157
+
1158
+ # Update estimates
1159
+ var_a_init = max(0.01, var_a_new)
1160
+ var_d_init = max(0.01, var_d_new)
1161
+ var_e_init = max(0.01, var_e_new)
1162
+
1163
+ # Final variance components
1164
+ var_a = var_a_init
1165
+ var_d = var_d_init
1166
+ var_e = var_e_init
1167
+
1168
+ # Calculate heritabilities
1169
+ narrow_heritability = var_a / (var_a + var_d + var_e)
1170
+ broad_heritability = (var_a + var_d) / (var_a + var_d + var_e)
1171
+
1172
+ # BLUP solutions for breeding values and dominance deviations
1173
+ V = var_a * G_a + var_d * G_d + var_e * np.eye(n_individuals)
1174
+ V_inv = linalg.inv(V)
1175
+ breeding_values = var_a * G_a @ V_inv @ y_adj
1176
+ dominance_deviations = var_d * G_d @ V_inv @ y_adj
1177
+
1178
+ # Predicted phenotypes
1179
+ predicted_phenotypes = breeding_values + dominance_deviations
1180
+
1181
+ # Calculate accuracy
1182
+ accuracy = np.corrcoef(trait_phenotypes, predicted_phenotypes)[0, 1]
1183
+
1184
+ # Log results
1185
+ log += f"Estimated additive genetic variance: {var_a:.4f}\n"
1186
+ log += f"Estimated dominance genetic variance: {var_d:.4f}\n"
1187
+ log += f"Estimated residual variance: {var_e:.4f}\n"
1188
+ log += f"Estimated narrow-sense heritability: {narrow_heritability:.4f}\n"
1189
+ log += f"Estimated broad-sense heritability: {broad_heritability:.4f}\n"
1190
+ log += f"Prediction accuracy (correlation): {accuracy:.4f}\n\n"
1191
+
1192
+ # Store results
1193
+ trait_result = {
1194
+ "trait": trait_idx + 1,
1195
+ "var_a": var_a,
1196
+ "var_d": var_d,
1197
+ "var_e": var_e,
1198
+ "narrow_heritability": narrow_heritability,
1199
+ "broad_heritability": broad_heritability,
1200
+ "accuracy": accuracy,
1201
+ "breeding_values": breeding_values,
1202
+ "dominance_deviations": dominance_deviations,
1203
+ "predicted_phenotypes": predicted_phenotypes,
1204
+ }
1205
+ trait_results.append(trait_result)
1206
+
1207
+ # Save results to file
1208
+ results_df = pd.DataFrame()
1209
+
1210
+ for i, trait_result in enumerate(trait_results):
1211
+ # Create individual-level results
1212
+ ind_data = {
1213
+ "individual": np.arange(1, n_individuals + 1),
1214
+ f"trait_{i + 1}_observed": phenotypes[:, i],
1215
+ f"trait_{i + 1}_predicted": trait_result["predicted_phenotypes"],
1216
+ f"trait_{i + 1}_breeding_value": trait_result["breeding_values"],
1217
+ }
1218
+
1219
+ if model_type == "additive_dominance":
1220
+ ind_data[f"trait_{i + 1}_dominance_deviation"] = trait_result["dominance_deviations"]
1221
+
1222
+ # Create or append to dataframe
1223
+ if i == 0:
1224
+ results_df = pd.DataFrame(ind_data)
1225
+ else:
1226
+ for key, value in ind_data.items():
1227
+ if key != "individual": # Skip duplicating individual column
1228
+ results_df[key] = value
1229
+
1230
+ # Save to CSV
1231
+ results_df.to_csv(output_file, index=False)
1232
+ log += f"Results saved to {output_file}\n"
1233
+
1234
+ return log
1235
+
1236
+
1237
+ def perform_pcr_and_gel_electrophoresis(
1238
+ genomic_dna,
1239
+ forward_primer=None,
1240
+ reverse_primer=None,
1241
+ target_region=None,
1242
+ annealing_temp=58,
1243
+ extension_time=30,
1244
+ cycles=35,
1245
+ gel_percentage=2.0,
1246
+ output_prefix="pcr_result",
1247
+ ):
1248
+ """Performs PCR amplification of a target transgene and visualizes results using agarose gel electrophoresis.
1249
+
1250
+ Parameters
1251
+ ----------
1252
+ genomic_dna : str
1253
+ Path to file containing genomic DNA sequence in FASTA format or the sequence itself
1254
+ forward_primer : str, optional
1255
+ Forward primer sequence. If not provided, will be designed based on target_region
1256
+ reverse_primer : str, optional
1257
+ Reverse primer sequence. If not provided, will be designed based on target_region
1258
+ target_region : tuple, optional
1259
+ Tuple of (start, end) positions for the target region in the genomic DNA
1260
+ annealing_temp : float, default=58
1261
+ Annealing temperature for PCR in °C
1262
+ extension_time : int, default=30
1263
+ Extension time in seconds
1264
+ cycles : int, default=35
1265
+ Number of PCR cycles
1266
+ gel_percentage : float, default=2.0
1267
+ Percentage of agarose gel
1268
+ output_prefix : str, default="pcr_result"
1269
+ Prefix for output files
1270
+
1271
+ Returns
1272
+ -------
1273
+ str
1274
+ Research log summarizing the PCR and gel electrophoresis procedures and results
1275
+
1276
+ """
1277
+ import datetime
1278
+
1279
+ import matplotlib.pyplot as plt
1280
+ import numpy as np
1281
+ from Bio import SeqIO
1282
+ from Bio.Seq import Seq
1283
+
1284
+ log = f"PCR AMPLIFICATION AND GEL ELECTROPHORESIS LOG - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
1285
+ log += "=" * 80 + "\n\n"
1286
+
1287
+ # Step 1: Load the genomic DNA
1288
+ log += "STEP 1: PREPARING GENOMIC DNA\n"
1289
+ if os.path.isfile(genomic_dna):
1290
+ try:
1291
+ record = SeqIO.read(genomic_dna, "fasta")
1292
+ dna_sequence = str(record.seq)
1293
+ log += f"- Loaded genomic DNA from file: {genomic_dna}\n"
1294
+ log += f"- Sequence length: {len(dna_sequence)} bp\n"
1295
+ except Exception as e:
1296
+ log += f"- Error loading DNA file: {str(e)}\n"
1297
+ return log
1298
+ else:
1299
+ dna_sequence = genomic_dna
1300
+ log += "- Using provided DNA sequence\n"
1301
+ log += f"- Sequence length: {len(dna_sequence)} bp\n"
1302
+
1303
+ log += "\n"
1304
+
1305
+ # Step 2: Design or validate primers
1306
+ log += "STEP 2: PCR PRIMER PREPARATION\n"
1307
+
1308
+ if forward_primer is None or reverse_primer is None:
1309
+ if target_region is None:
1310
+ log += "- Error: Either primers or target region must be provided\n"
1311
+ return log
1312
+
1313
+ # Simple primer design based on target region
1314
+ start, end = target_region
1315
+
1316
+ if forward_primer is None:
1317
+ # Take 20bp from the start of the target region for forward primer
1318
+ forward_primer = dna_sequence[start : start + 20]
1319
+ log += "- Designed forward primer based on target region\n"
1320
+
1321
+ if reverse_primer is None:
1322
+ # Take 20bp from the end of the target region for reverse primer (reverse complement)
1323
+ reverse_seq = Seq(dna_sequence[end - 20 : end])
1324
+ reverse_primer = str(reverse_seq.reverse_complement())
1325
+ log += "- Designed reverse primer based on target region\n"
1326
+
1327
+ log += f"- Forward primer: 5'-{forward_primer}-3' ({len(forward_primer)} bp)\n"
1328
+ log += f"- Reverse primer: 5'-{reverse_primer}-3' ({len(reverse_primer)} bp)\n"
1329
+
1330
+ # Step 3: PCR Setup and Amplification
1331
+ log += "\nSTEP 3: PCR AMPLIFICATION\n"
1332
+ log += "- PCR reaction setup:\n"
1333
+ log += " * Template DNA: Genomic DNA\n"
1334
+ log += f" * Forward primer: 5'-{forward_primer}-3'\n"
1335
+ log += f" * Reverse primer: 5'-{reverse_primer}-3'\n"
1336
+ log += f" * Annealing temperature: {annealing_temp}°C\n"
1337
+ log += f" * Extension time: {extension_time} seconds\n"
1338
+ log += f" * Number of cycles: {cycles}\n"
1339
+
1340
+ # Simulate PCR by finding binding sites and determining amplicon size
1341
+ amplicon_size = None
1342
+ amplicon_sequence = None
1343
+
1344
+ # Find forward primer binding site
1345
+ fwd_pos = dna_sequence.find(forward_primer)
1346
+ if fwd_pos == -1:
1347
+ log += "- Warning: Forward primer binding site not found in sequence\n"
1348
+
1349
+ # Find reverse primer binding site (need to search for reverse complement)
1350
+ rev_primer_seq = Seq(reverse_primer)
1351
+ rev_primer_rc = str(rev_primer_seq.reverse_complement())
1352
+ rev_pos = dna_sequence.find(rev_primer_rc)
1353
+ if rev_pos == -1:
1354
+ log += "- Warning: Reverse primer binding site not found in sequence\n"
1355
+
1356
+ # If we found both binding sites, calculate amplicon size
1357
+ if fwd_pos != -1 and rev_pos != -1:
1358
+ if fwd_pos < rev_pos:
1359
+ amplicon_size = rev_pos + len(reverse_primer) - fwd_pos
1360
+ amplicon_sequence = dna_sequence[fwd_pos : rev_pos + len(reverse_primer)]
1361
+ log += "- PCR amplification successful\n"
1362
+ log += f"- Amplicon size: {amplicon_size} bp\n"
1363
+ else:
1364
+ log += "- Error: Primer binding sites are in incorrect orientation\n"
1365
+ # Simulate based on target region if provided
1366
+ elif target_region is not None:
1367
+ start, end = target_region
1368
+ amplicon_size = end - start + len(forward_primer) + len(reverse_primer)
1369
+ log += "- PCR amplification simulated based on target region\n"
1370
+ log += f"- Expected amplicon size: {amplicon_size} bp\n"
1371
+ else:
1372
+ log += "- PCR amplification failed: could not determine amplicon size\n"
1373
+ return log
1374
+
1375
+ # Step 4: Gel Electrophoresis
1376
+ log += "\nSTEP 4: AGAROSE GEL ELECTROPHORESIS\n"
1377
+ log += f"- Prepared {gel_percentage}% agarose gel\n"
1378
+ log += "- Loaded PCR product alongside DNA ladder\n"
1379
+ log += "- Ran electrophoresis at 100V for 45 minutes\n"
1380
+
1381
+ # Create a simulated gel image
1382
+ fig, ax = plt.subplots(figsize=(6, 8))
1383
+
1384
+ # Draw gel lanes
1385
+ ax.add_patch(plt.Rectangle((0, 0), 6, 10, color="lightgray", alpha=0.5))
1386
+
1387
+ # DNA Ladder (100bp increments)
1388
+ ladder_sizes = [100, 200, 300, 500, 700, 1000, 1500, 2000]
1389
+ ladder_positions = [10 - (np.log(size) / np.log(2000) * 8) for size in ladder_sizes]
1390
+
1391
+ # Plot ladder
1392
+ for pos, size in zip(ladder_positions, ladder_sizes, strict=False):
1393
+ ax.add_patch(plt.Rectangle((0.5, pos - 0.1), 1, 0.2, color="black", alpha=0.8))
1394
+ ax.text(0.2, pos, f"{size}bp", fontsize=8, ha="right", va="center")
1395
+
1396
+ # Plot sample band
1397
+ if amplicon_size:
1398
+ sample_position = 10 - (np.log(amplicon_size) / np.log(2000) * 8)
1399
+ ax.add_patch(plt.Rectangle((3.5, sample_position - 0.15), 1, 0.3, color="black", alpha=0.8))
1400
+ ax.text(
1401
+ 4.5,
1402
+ sample_position,
1403
+ f"{amplicon_size}bp",
1404
+ fontsize=8,
1405
+ ha="left",
1406
+ va="center",
1407
+ )
1408
+
1409
+ # Set up the plot
1410
+ ax.set_xlim(0, 6)
1411
+ ax.set_ylim(0, 10)
1412
+ ax.set_xticks([0.5, 3.5])
1413
+ ax.set_xticklabels(["Ladder", "Sample"])
1414
+ ax.set_yticks([])
1415
+ ax.spines["top"].set_visible(False)
1416
+ ax.spines["right"].set_visible(False)
1417
+ ax.spines["left"].set_visible(False)
1418
+ ax.set_title(f"{gel_percentage}% Agarose Gel")
1419
+
1420
+ # Save the gel image
1421
+ gel_image_path = f"{output_prefix}_gel.png"
1422
+ plt.savefig(gel_image_path, dpi=300, bbox_inches="tight")
1423
+ plt.close()
1424
+
1425
+ log += f"- Gel image saved as: {gel_image_path}\n"
1426
+
1427
+ # Results interpretation
1428
+ log += "\nRESULTS INTERPRETATION:\n"
1429
+ if amplicon_size:
1430
+ log += f"- Detected band at approximately {amplicon_size} bp\n"
1431
+
1432
+ # Save amplicon sequence if available
1433
+ if amplicon_sequence:
1434
+ seq_file = f"{output_prefix}_amplicon.fasta"
1435
+ with open(seq_file, "w") as f:
1436
+ f.write(f">PCR_Amplicon_{amplicon_size}bp\n")
1437
+ f.write(amplicon_sequence)
1438
+ log += f"- Amplicon sequence saved as: {seq_file}\n"
1439
+ else:
1440
+ log += "- No bands detected\n"
1441
+
1442
+ return log
1443
+
1444
+
1445
+ def analyze_protein_phylogeny(
1446
+ fasta_sequences,
1447
+ output_dir="./",
1448
+ alignment_method="clustalw",
1449
+ tree_method="fasttree",
1450
+ ):
1451
+ """Perform phylogenetic analysis on a set of protein sequences.
1452
+
1453
+ This function takes protein sequences in FASTA format, performs multiple sequence alignment,
1454
+ constructs a phylogenetic tree, and visualizes the evolutionary relationships.
1455
+
1456
+ Parameters
1457
+ ----------
1458
+ fasta_sequences : str
1459
+ Path to a FASTA file containing protein sequences or a string with FASTA-formatted sequences
1460
+ output_dir : str, optional
1461
+ Directory to save output files (default: current directory)
1462
+ alignment_method : str, optional
1463
+ Method for sequence alignment: "clustalw", "muscle", or "pre-aligned" (default: "clustalw")
1464
+ tree_method : str, optional
1465
+ Method for tree construction: "iqtree" (default: "iqtree")
1466
+
1467
+ Returns
1468
+ -------
1469
+ str
1470
+ Research log summarizing the phylogenetic analysis process
1471
+
1472
+ """
1473
+ import datetime
1474
+ import subprocess
1475
+ import tempfile
1476
+
1477
+ from Bio import AlignIO, Phylo, SeqIO
1478
+ from Bio.Align.Applications import ClustalwCommandline, MuscleCommandline
1479
+
1480
+ # Create output directory if it doesn't exist
1481
+ if not os.path.exists(output_dir):
1482
+ os.makedirs(output_dir)
1483
+
1484
+ # Initialize log
1485
+ log = []
1486
+ log.append(f"Phylogenetic Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
1487
+ log.append("=" * 50)
1488
+
1489
+ # Check if input is a file path or string content
1490
+ if os.path.isfile(fasta_sequences):
1491
+ input_file = fasta_sequences
1492
+ log.append(f"Using sequences from file: {input_file}")
1493
+ else:
1494
+ # Create temporary file with the string content
1495
+ temp_fasta = tempfile.NamedTemporaryFile(delete=False, suffix=".fasta", dir=output_dir)
1496
+ temp_fasta.write(fasta_sequences.encode())
1497
+ temp_fasta.close()
1498
+ input_file = temp_fasta.name
1499
+ log.append(f"Created temporary FASTA file from provided sequences: {input_file}")
1500
+
1501
+ # Count sequences
1502
+ try:
1503
+ sequences = list(SeqIO.parse(input_file, "fasta"))
1504
+ log.append(f"Loaded {len(sequences)} protein sequences")
1505
+ except Exception as e:
1506
+ log.append(f"Warning: Could not parse sequences as FASTA: {str(e)}")
1507
+ # This might be pre-aligned data already
1508
+ sequences = []
1509
+
1510
+ # Create filenames for outputs
1511
+ base_name = os.path.splitext(os.path.basename(input_file))[0]
1512
+ alignment_file = os.path.join(output_dir, f"{base_name}_aligned.aln")
1513
+ tree_file = os.path.join(output_dir, f"{base_name}_tree.nwk")
1514
+ tree_image = os.path.join(output_dir, f"{base_name}_phylogeny.png")
1515
+
1516
+ # Perform multiple sequence alignment
1517
+ log.append("\nStep 1: Multiple Sequence Alignment")
1518
+
1519
+ # Special case for pre-aligned input
1520
+ if alignment_method.lower() == "pre-aligned":
1521
+ log.append("Using pre-aligned sequences")
1522
+ try:
1523
+ # If the input is already an alignment file, copy it to the alignment_file path
1524
+ with open(input_file) as src, open(alignment_file, "w") as dst:
1525
+ dst.write(src.read())
1526
+ log.append(f"Copied pre-aligned file to: {alignment_file}")
1527
+ except Exception as e:
1528
+ log.append(f"Error processing pre-aligned sequences: {str(e)}")
1529
+ return "\n".join(log)
1530
+ elif alignment_method.lower() == "clustalw":
1531
+ log.append("Using Clustal Omega for alignment")
1532
+ try:
1533
+ clustalw_cline = ClustalwCommandline("clustalw", infile=input_file, outfile=alignment_file)
1534
+ stdout, stderr = clustalw_cline()
1535
+ log.append("Alignment completed successfully")
1536
+ except Exception as e:
1537
+ log.append(f"Error during alignment: {str(e)}")
1538
+ # Try alternative approach using MUSCLE if ClustalW fails
1539
+ alignment_method = "muscle"
1540
+
1541
+ if alignment_method.lower() == "muscle":
1542
+ log.append("Using MUSCLE for alignment")
1543
+ try:
1544
+ muscle_cline = MuscleCommandline("muscle", input=input_file, out=alignment_file)
1545
+ stdout, stderr = muscle_cline()
1546
+ log.append("Alignment completed successfully")
1547
+ except Exception as e:
1548
+ log.append(f"Error during MUSCLE alignment: {str(e)}")
1549
+ log.append("Attempting to use Biopython's built-in pairwise2 alignment as fallback")
1550
+
1551
+ from Bio import pairwise2
1552
+
1553
+ # Create a simple progressive alignment
1554
+ ref_seq = sequences[0]
1555
+ alignments = []
1556
+
1557
+ for seq in sequences:
1558
+ alignment = pairwise2.align.globalxx(ref_seq.seq, seq.seq)[0]
1559
+ alignments.append(alignment)
1560
+
1561
+ # Write alignment to file in CLUSTAL format
1562
+ with open(alignment_file, "w") as f:
1563
+ f.write("CLUSTAL W (1.83) multiple sequence alignment\n\n")
1564
+ for i, seq in enumerate(sequences):
1565
+ f.write(f"{seq.id.ljust(10)} {alignments[i][1]}\n")
1566
+ # Add consensus line with asterisks
1567
+ f.write(" " * 10 + " " * len(alignments[0][1]) + "\n")
1568
+
1569
+ log.append("Created basic alignment using Biopython's pairwise2")
1570
+
1571
+ # Verify alignment file exists
1572
+ if not os.path.exists(alignment_file):
1573
+ log.append(f"Error: Alignment file {alignment_file} does not exist")
1574
+ return "\n".join(log)
1575
+
1576
+ # Build phylogenetic tree
1577
+ log.append("\nStep 2: Phylogenetic Tree Construction")
1578
+
1579
+ if tree_method.lower() == "iqtree":
1580
+ log.append("Using IQ-TREE for phylogenetic tree construction")
1581
+ try:
1582
+ cmd = f"iqtree -s {alignment_file} -m LG -bb 1000 -pre {os.path.join(output_dir, base_name)}"
1583
+ subprocess.run(
1584
+ cmd,
1585
+ shell=True,
1586
+ check=True,
1587
+ capture_output=True,
1588
+ )
1589
+ # IQ-TREE creates files with .treefile extension
1590
+ iqtree_file = os.path.join(output_dir, f"{base_name}.treefile")
1591
+ if os.path.exists(iqtree_file):
1592
+ # Rename to our standard name
1593
+ os.rename(iqtree_file, tree_file)
1594
+ log.append("Tree construction completed successfully")
1595
+ except Exception as e:
1596
+ log.append(f"Error during IQ-TREE execution: {str(e)}")
1597
+ log.append("Falling back to neighbor-joining method")
1598
+
1599
+ try:
1600
+ from Bio.Phylo.TreeConstruction import (
1601
+ DistanceCalculator,
1602
+ DistanceTreeConstructor,
1603
+ )
1604
+
1605
+ # Try to read the alignment in different formats
1606
+ alignment = None
1607
+ for format in ["clustal", "fasta"]:
1608
+ try:
1609
+ alignment = AlignIO.read(alignment_file, format)
1610
+ break
1611
+ except Exception:
1612
+ continue
1613
+
1614
+ if alignment is None:
1615
+ log.append("Could not parse alignment file in any supported format")
1616
+ # Create a simple text-based tree file as a fallback
1617
+ with open(tree_file, "w") as f:
1618
+ f.write("(protein1:0.1,protein2:0.2,(protein3:0.3,protein4:0.4):0.5);")
1619
+ log.append("Created placeholder tree file")
1620
+ else:
1621
+ # Calculate the distance matrix
1622
+ calculator = DistanceCalculator("identity")
1623
+ dm = calculator.get_distance(alignment)
1624
+
1625
+ # Construct the tree
1626
+ constructor = DistanceTreeConstructor()
1627
+ tree = constructor.nj(dm)
1628
+
1629
+ # Write the tree to file
1630
+ Phylo.write(tree, tree_file, "newick")
1631
+ log.append("Created tree using neighbor-joining method")
1632
+ except Exception as e:
1633
+ log.append(f"Error during fallback tree construction: {str(e)}")
1634
+ # Create a simple text-based tree file as a final fallback
1635
+ with open(tree_file, "w") as f:
1636
+ f.write("(protein1:0.1,protein2:0.2,(protein3:0.3,protein4:0.4):0.5);")
1637
+ log.append("Created placeholder tree file as final fallback")
1638
+ else:
1639
+ log.append(f"Unsupported tree method: {tree_method}")
1640
+ return "\n".join(log)
1641
+
1642
+ # Verify tree file exists
1643
+ if not os.path.exists(tree_file):
1644
+ log.append(f"Error: Tree file {tree_file} does not exist")
1645
+ return "\n".join(log)
1646
+
1647
+ # Visualize the tree
1648
+ log.append("\nStep 3: Phylogenetic Tree Visualization")
1649
+ try:
1650
+ import matplotlib
1651
+
1652
+ matplotlib.use("Agg") # Use non-interactive backend
1653
+ import matplotlib.pyplot as plt
1654
+
1655
+ tree = Phylo.read(tree_file, "newick")
1656
+ fig = plt.figure(figsize=(10, len(sequences) * 0.3 if sequences else 5))
1657
+ axes = fig.add_subplot(1, 1, 1)
1658
+ Phylo.draw(tree, axes=axes, do_show=False)
1659
+ plt.savefig(tree_image, dpi=300, bbox_inches="tight")
1660
+ plt.close()
1661
+ log.append(f"Tree visualization saved to: {tree_image}")
1662
+ except Exception as e:
1663
+ log.append(f"Error during tree visualization: {str(e)}")
1664
+
1665
+ # Summary
1666
+ log.append("\nSummary:")
1667
+ log.append(f"- Input sequences: {len(sequences) if sequences else 'pre-aligned data'}")
1668
+ log.append(f"- Alignment file: {alignment_file}")
1669
+ log.append(f"- Phylogenetic tree file: {tree_file}")
1670
+ log.append(f"- Tree visualization: {tree_image}")
1671
+
1672
+ return "\n".join(log)
Biomni/biomni/tool/literature.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import time
4
+ from io import BytesIO
5
+ from urllib.parse import urljoin
6
+
7
+ import PyPDF2
8
+ import requests
9
+ from bs4 import BeautifulSoup
10
+ from googlesearch import search
11
+
12
+
13
+ def fetch_supplementary_info_from_doi(doi: str, output_dir: str = "supplementary_info"):
14
+ """Fetches supplementary information for a paper given its DOI and returns a research log.
15
+
16
+ Args:
17
+ doi: The paper DOI.
18
+ output_dir: Directory to save supplementary files.
19
+
20
+ Returns:
21
+ dict: A dictionary containing a research log and the downloaded file paths.
22
+
23
+ """
24
+ research_log = []
25
+ research_log.append(f"Starting process for DOI: {doi}")
26
+
27
+ # CrossRef API to resolve DOI to a publisher page
28
+ crossref_url = f"https://doi.org/{doi}"
29
+ headers = {"User-Agent": "Mozilla/5.0"}
30
+ response = requests.get(crossref_url, headers=headers)
31
+
32
+ if response.status_code != 200:
33
+ log_message = f"Failed to resolve DOI: {doi}. Status Code: {response.status_code}"
34
+ research_log.append(log_message)
35
+ return {"log": research_log, "files": []}
36
+
37
+ publisher_url = response.url
38
+ research_log.append(f"Resolved DOI to publisher page: {publisher_url}")
39
+
40
+ # Fetch publisher page
41
+ response = requests.get(publisher_url, headers=headers)
42
+ if response.status_code != 200:
43
+ log_message = f"Failed to access publisher page for DOI {doi}."
44
+ research_log.append(log_message)
45
+ return {"log": research_log, "files": []}
46
+
47
+ # Parse page content
48
+ soup = BeautifulSoup(response.content, "html.parser")
49
+ supplementary_links = []
50
+
51
+ # Look for supplementary materials by keywords or links
52
+ for link in soup.find_all("a", href=True):
53
+ href = link.get("href")
54
+ text = link.get_text().lower()
55
+ if "supplementary" in text or "supplemental" in text or "appendix" in text:
56
+ full_url = urljoin(publisher_url, href)
57
+ supplementary_links.append(full_url)
58
+ research_log.append(f"Found supplementary material link: {full_url}")
59
+
60
+ if not supplementary_links:
61
+ log_message = f"No supplementary materials found for DOI {doi}."
62
+ research_log.append(log_message)
63
+ return research_log
64
+
65
+ # Create output directory
66
+ os.makedirs(output_dir, exist_ok=True)
67
+ research_log.append(f"Created output directory: {output_dir}")
68
+
69
+ # Download supplementary materials
70
+ downloaded_files = []
71
+ for link in supplementary_links:
72
+ file_name = os.path.join(output_dir, link.split("/")[-1])
73
+ file_response = requests.get(link, headers=headers)
74
+ if file_response.status_code == 200:
75
+ with open(file_name, "wb") as f:
76
+ f.write(file_response.content)
77
+ downloaded_files.append(file_name)
78
+ research_log.append(f"Downloaded file: {file_name}")
79
+ else:
80
+ research_log.append(f"Failed to download file from {link}")
81
+
82
+ if downloaded_files:
83
+ research_log.append(f"Successfully downloaded {len(downloaded_files)} file(s).")
84
+ else:
85
+ research_log.append(f"No files could be downloaded for DOI {doi}.")
86
+
87
+ return "\n".join(research_log)
88
+
89
+
90
+ def query_arxiv(query: str, max_papers: int = 10) -> str:
91
+ """Query arXiv for papers based on the provided search query.
92
+
93
+ Parameters
94
+ ----------
95
+ - query (str): The search query string.
96
+ - max_papers (int): The maximum number of papers to retrieve (default: 10).
97
+
98
+ Returns
99
+ -------
100
+ - str: The formatted search results or an error message.
101
+
102
+ """
103
+ import arxiv
104
+
105
+ try:
106
+ client = arxiv.Client()
107
+ search = arxiv.Search(query=query, max_results=max_papers, sort_by=arxiv.SortCriterion.Relevance)
108
+ results = "\n\n".join([f"Title: {paper.title}\nSummary: {paper.summary}" for paper in client.results(search)])
109
+ return results if results else "No papers found on arXiv."
110
+ except Exception as e:
111
+ return f"Error querying arXiv: {e}"
112
+
113
+
114
+ def query_scholar(query: str) -> str:
115
+ """Query Google Scholar for papers based on the provided search query.
116
+
117
+ Parameters
118
+ ----------
119
+ - query (str): The search query string.
120
+
121
+ Returns
122
+ -------
123
+ - str: The first search result formatted or an error message.
124
+
125
+ """
126
+ from scholarly import ProxyGenerator, scholarly
127
+
128
+ # Set up a ProxyGenerator object to use free proxies
129
+ # This needs to be done only once per session
130
+ pg = ProxyGenerator()
131
+ pg.FreeProxies()
132
+ scholarly.use_proxy(pg)
133
+ try:
134
+ search_query = scholarly.search_pubs(query)
135
+ result = next(search_query, None)
136
+ if result:
137
+ return f"Title: {result['bib']['title']}\nYear: {result['bib']['pub_year']}\nVenue: {result['bib']['venue']}\nAbstract: {result['bib']['abstract']}"
138
+ else:
139
+ return "No results found on Google Scholar."
140
+ except Exception as e:
141
+ return f"Error querying Google Scholar: {e}"
142
+
143
+
144
+ def query_pubmed(query: str, max_papers: int = 10, max_retries: int = 3) -> str:
145
+ """Query PubMed for papers based on the provided search query.
146
+
147
+ Parameters
148
+ ----------
149
+ - query (str): The search query string.
150
+ - max_papers (int): The maximum number of papers to retrieve (default: 10).
151
+ - max_retries (int): Maximum number of retry attempts with modified queries (default: 3).
152
+
153
+ Returns
154
+ -------
155
+ - str: The formatted search results or an error message.
156
+
157
+ """
158
+ from pymed import PubMed
159
+
160
+ try:
161
+ pubmed = PubMed(tool="MyTool", email="your-email@example.com") # Update with a valid email address
162
+
163
+ # Initial attempt
164
+ papers = list(pubmed.query(query, max_results=max_papers))
165
+
166
+ # Retry with modified queries if no results
167
+ retries = 0
168
+ while not papers and retries < max_retries:
169
+ retries += 1
170
+ # Simplify query with each retry by removing the last word
171
+ simplified_query = " ".join(query.split()[:-retries]) if len(query.split()) > retries else query
172
+ time.sleep(1) # Add delay between requests
173
+ papers = list(pubmed.query(simplified_query, max_results=max_papers))
174
+
175
+ if papers:
176
+ results = "\n\n".join(
177
+ [f"Title: {paper.title}\nAbstract: {paper.abstract}\nJournal: {paper.journal}" for paper in papers]
178
+ )
179
+ return results
180
+ else:
181
+ return "No papers found on PubMed after multiple query attempts."
182
+ except Exception as e:
183
+ return f"Error querying PubMed: {e}"
184
+
185
+
186
+ def search_google(query: str, num_results: int = 3, language: str = "en") -> list[dict]:
187
+ """Search using Google search.
188
+
189
+ Args:
190
+ query (str): The search query (e.g., "protocol text or seach question")
191
+ num_results (int): Number of results to return (default: 10)
192
+ language (str): Language code for search results (default: 'en')
193
+ pause (float): Pause between searches to avoid rate limiting (default: 2.0 seconds)
194
+
195
+ Returns:
196
+ List[dict]: List of dictionaries containing search results with title and URL
197
+
198
+ """
199
+ try:
200
+ results_string = ""
201
+ search_query = f"{query}"
202
+
203
+ print(f"Searching for {search_query} with {num_results} results and {language} language")
204
+
205
+ for res in search(search_query, num_results=num_results, lang=language, advanced=True):
206
+ print(f"Found result: {res.title}")
207
+ title = res.title
208
+ url = res.url
209
+ description = res.description
210
+
211
+ results_string += f"Title: {title}\nURL: {url}\nDescription: {description}\n\n"
212
+
213
+ except Exception as e:
214
+ print(f"Error performing search: {str(e)}")
215
+ return results_string
216
+
217
+
218
+ def advanced_web_search_claude(
219
+ query: str,
220
+ max_searches: int = 1,
221
+ max_retries: int = 3,
222
+ ) -> tuple[str, list[dict[str, str]], list]:
223
+ """
224
+ Initiate an advanced web search by launching a specialized agent to collect relevant information and citations through multiple rounds of web searches for a given query.
225
+ Craft the query carefully for the search agent to find the most relevant information.
226
+
227
+ Parameters
228
+ ----------
229
+ query : str
230
+ The search phrase you want Claude to look up.
231
+ max_searches : int, optional
232
+ Upper-bound on searches Claude may issue inside this request.
233
+ max_retries : int, optional
234
+ Maximum number of retry attempts with exponential backoff.
235
+
236
+ Returns
237
+ -------
238
+ full_text : str
239
+ A formatted string containing the full text response from Claude and the citations.
240
+ """
241
+ import random
242
+
243
+ import anthropic
244
+
245
+ try:
246
+ from biomni.config import default_config
247
+
248
+ model = default_config.llm
249
+ api_key = default_config.api_key
250
+ if not api_key:
251
+ api_key = os.getenv("ANTHROPIC_API_KEY")
252
+ except ImportError:
253
+ model = "claude-4-sonnet-latest"
254
+ api_key = os.getenv("ANTHROPIC_API_KEY")
255
+
256
+ if "claude" not in model:
257
+ raise ValueError("Model must be a Claude model.")
258
+
259
+ if not api_key:
260
+ raise ValueError("Set your api_key explicitly.")
261
+
262
+ client = anthropic.Anthropic(api_key=api_key)
263
+ tool_def = {
264
+ "type": "web_search_20250305",
265
+ "name": "web_search",
266
+ "max_uses": max_searches,
267
+ }
268
+
269
+ delay = random.randint(1, 10)
270
+
271
+ for attempt in range(1, max_retries + 1):
272
+ try:
273
+ response = client.messages.create(
274
+ model=model,
275
+ max_tokens=4096,
276
+ messages=[{"role": "user", "content": query}],
277
+ tools=[tool_def],
278
+ )
279
+
280
+ paragraphs, citations = [], []
281
+ response.content = response.content
282
+ formatted_response = ""
283
+ for blk in response.content:
284
+ if blk.type == "text":
285
+ paragraphs.append(blk.text)
286
+ formatted_response += blk.text
287
+
288
+ if blk.citations:
289
+ for cite in blk.citations:
290
+ citations.append({"url": cite.url, "title": cite.title, "cited_text": cite.cited_text})
291
+ formatted_response += f"(Citation: {cite.title} - {cite.url})"
292
+ return formatted_response
293
+
294
+ except Exception as e:
295
+ if attempt < max_retries:
296
+ time.sleep(delay)
297
+ delay *= 2
298
+ continue
299
+ print(f"Error performing web search after {max_retries} attempts: {str(e)}")
300
+ return f"Error performing web search after {max_retries} attempts: {str(e)}"
301
+
302
+
303
+ def extract_url_content(url: str) -> str:
304
+ """Extract the text content of a webpage using requests and BeautifulSoup.
305
+
306
+ Args:
307
+ url: Webpage URL to extract content from
308
+
309
+ Returns:
310
+ Text content of the webpage
311
+
312
+ """
313
+ response = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
314
+
315
+ # Check if the response is in text format
316
+ if "text/plain" in response.headers.get("Content-Type", "") or "application/json" in response.headers.get(
317
+ "Content-Type", ""
318
+ ):
319
+ return response.text.strip() # Return plain text or JSON response directly
320
+
321
+ # If it's HTML, use BeautifulSoup to parse
322
+ soup = BeautifulSoup(response.text, "html.parser")
323
+
324
+ # Try to find main content first, fallback to body
325
+ content = soup.find("main") or soup.find("article") or soup.body
326
+
327
+ # Remove unwanted elements
328
+ for element in content(["script", "style", "nav", "header", "footer", "aside", "iframe"]):
329
+ element.decompose()
330
+
331
+ # Extract text with better formatting
332
+ paragraphs = content.find_all(["p", "h1", "h2", "h3", "h4", "h5", "h6"])
333
+ cleaned_text = []
334
+
335
+ for p in paragraphs:
336
+ text = p.get_text().strip()
337
+ if text: # Only add non-empty paragraphs
338
+ cleaned_text.append(text)
339
+
340
+ return "\n\n".join(cleaned_text)
341
+
342
+
343
+ def extract_pdf_content(url: str) -> str:
344
+ """Extract the text content of a PDF file given its URL.
345
+
346
+ Args:
347
+ url: URL of the PDF file to extract text from
348
+
349
+ Returns:
350
+ The extracted text content from the PDF
351
+
352
+ """
353
+ try:
354
+ # Check if the URL ends with .pdf
355
+ if not url.lower().endswith(".pdf"):
356
+ # If not, try to find a PDF link on the page
357
+ response = requests.get(url, timeout=30)
358
+ if response.status_code == 200:
359
+ # Look for PDF links in the HTML content
360
+ pdf_links = re.findall(r'href=[\'"]([^\'"]+\.pdf)[\'"]', response.text)
361
+ if pdf_links:
362
+ # Use the first PDF link found
363
+ if not pdf_links[0].startswith("http"):
364
+ # Handle relative URLs
365
+ base_url = "/".join(url.split("/")[:3])
366
+ url = base_url + pdf_links[0] if pdf_links[0].startswith("/") else base_url + "/" + pdf_links[0]
367
+ else:
368
+ url = pdf_links[0]
369
+ else:
370
+ return f"No PDF file found at {url}. Please provide a direct link to a PDF file."
371
+
372
+ # Download the PDF
373
+ response = requests.get(url, timeout=30)
374
+
375
+ # Check if we actually got a PDF file (by checking content type or magic bytes)
376
+ content_type = response.headers.get("Content-Type", "").lower()
377
+ if "application/pdf" not in content_type and not response.content.startswith(b"%PDF"):
378
+ return f"The URL did not return a valid PDF file. Content type: {content_type}"
379
+
380
+ pdf_file = BytesIO(response.content)
381
+
382
+ # Try with PyPDF2 first
383
+ try:
384
+ text = ""
385
+ pdf_reader = PyPDF2.PdfReader(pdf_file)
386
+ for page_num in range(len(pdf_reader.pages)):
387
+ page = pdf_reader.pages[page_num]
388
+ text += page.extract_text() + "\n\n"
389
+ except Exception as e:
390
+ print(f"Error extracting text from PDF: {str(e)}")
391
+
392
+ # Clean up the text
393
+ text = re.sub(r"\s+", " ", text).strip()
394
+
395
+ if not text:
396
+ return "The PDF file did not contain any extractable text. It may be an image-based PDF requiring OCR."
397
+
398
+ return text
399
+
400
+ except requests.exceptions.RequestException as e:
401
+ return f"Error downloading PDF: {str(e)}"
402
+ except Exception as e:
403
+ return f"Error extracting text from PDF: {str(e)}"
Biomni/biomni/tool/pathology.py ADDED
@@ -0,0 +1,1034 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def analyze_aortic_diameter_and_geometry(image_path, output_dir="./output"):
2
+ """Analyze aortic diameter and geometry from cardiovascular imaging data.
3
+
4
+ This function processes cardiovascular imaging data (ultrasound or CT/MRI) to
5
+ measure aortic root diameter, ascending aorta diameter, and calculate
6
+ geometric parameters such as tortuosity and dilation indices.
7
+
8
+ Parameters
9
+ ----------
10
+ image_path : str
11
+ Path to the cardiovascular imaging data (DICOM, JPG, PNG)
12
+ output_dir : str, optional
13
+ Directory to save output files (default: "./output")
14
+
15
+ Returns
16
+ -------
17
+ str
18
+ Research log summarizing the analysis steps and results
19
+
20
+ """
21
+ import os
22
+ from datetime import datetime
23
+
24
+ import cv2
25
+ import numpy as np
26
+
27
+ # Create output directory if it doesn't exist
28
+ os.makedirs(output_dir, exist_ok=True)
29
+
30
+ # Initialize research log
31
+ log = []
32
+ log.append(f"Aortic Diameter and Geometry Analysis - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
33
+ log.append(f"Input image: {image_path}")
34
+
35
+ # Step 1: Load the image
36
+ log.append("\n1. Loading and preprocessing image")
37
+ try:
38
+ # Check if file exists
39
+ if not os.path.exists(image_path):
40
+ return f"Error: File {image_path} not found"
41
+
42
+ # Load image
43
+ image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
44
+ if image is None:
45
+ return f"Error: Could not read image {image_path}"
46
+
47
+ log.append(f" - Image loaded: {image.shape[1]}x{image.shape[0]} pixels")
48
+
49
+ # Basic preprocessing
50
+ image = cv2.GaussianBlur(image, (5, 5), 0)
51
+ log.append(" - Applied Gaussian blur for noise reduction")
52
+
53
+ # Enhance contrast
54
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
55
+ image = clahe.apply(image)
56
+ log.append(" - Enhanced contrast using CLAHE")
57
+
58
+ except Exception as e:
59
+ return f"Error during image loading: {str(e)}"
60
+
61
+ # Step 2: Segment the aorta
62
+ log.append("\n2. Aorta segmentation")
63
+ try:
64
+ # Otsu thresholding for initial segmentation
65
+ thresh_val, binary = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
66
+ log.append(f" - Applied Otsu thresholding (threshold value: {thresh_val})")
67
+
68
+ # Morphological operations to clean up the binary image
69
+ kernel = np.ones((3, 3), np.uint8)
70
+ binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
71
+ binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
72
+ log.append(" - Applied morphological operations to refine segmentation")
73
+
74
+ # Find contours
75
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
76
+ log.append(f" - Detected {len(contours)} contours")
77
+
78
+ # Assuming the aorta is one of the largest contours
79
+ if len(contours) == 0:
80
+ return "Error: No contours detected in the image"
81
+
82
+ # Sort contours by area and take the largest
83
+ contours = sorted(contours, key=cv2.contourArea, reverse=True)
84
+ aorta_contour = contours[0] # Assuming the largest contour is the aorta
85
+ log.append(f" - Selected largest contour as aorta (area: {cv2.contourArea(aorta_contour)} px²)")
86
+
87
+ except Exception as e:
88
+ return f"Error during aorta segmentation: {str(e)}"
89
+
90
+ # Step 3: Measure aortic diameters
91
+ log.append("\n3. Measuring aortic diameters")
92
+ try:
93
+ # Create a mask of the aorta
94
+ aorta_mask = np.zeros_like(image)
95
+ cv2.drawContours(aorta_mask, [aorta_contour], 0, 255, -1)
96
+
97
+ # Find the centerline of the aorta (simplified approach)
98
+ # This is a simplified approach - in a real implementation, skeletonization would be better
99
+ M = cv2.moments(aorta_contour)
100
+ if M["m00"] != 0:
101
+ cx = int(M["m10"] / M["m00"])
102
+ cy = int(M["m01"] / M["m00"])
103
+ log.append(f" - Aorta centroid: ({cx}, {cy})")
104
+ else:
105
+ cx, cy = 0, 0
106
+ log.append(" - Warning: Could not calculate aorta centroid")
107
+
108
+ # Measure aortic root diameter (assuming it's at the bottom of the contour)
109
+ aortic_root_points = sorted(aorta_contour.reshape(-1, 2), key=lambda p: p[1], reverse=True)[:20]
110
+ aortic_root_diameter = np.max([p[0] for p in aortic_root_points]) - np.min([p[0] for p in aortic_root_points])
111
+ log.append(f" - Aortic root diameter: {aortic_root_diameter:.2f} pixels")
112
+
113
+ # Measure ascending aorta diameter (assuming it's at the middle of the contour)
114
+ y_mid = (
115
+ np.min([p[1] for p in aorta_contour.reshape(-1, 2)]) + np.max([p[1] for p in aorta_contour.reshape(-1, 2)])
116
+ ) // 2
117
+ ascending_points = [p for p in aorta_contour.reshape(-1, 2) if abs(p[1] - y_mid) < 10]
118
+ if ascending_points:
119
+ ascending_diameter = np.max([p[0] for p in ascending_points]) - np.min([p[0] for p in ascending_points])
120
+ log.append(f" - Ascending aorta diameter: {ascending_diameter:.2f} pixels")
121
+ else:
122
+ ascending_diameter = 0
123
+ log.append(" - Warning: Could not measure ascending aorta diameter")
124
+
125
+ except Exception as e:
126
+ return f"Error during diameter measurements: {str(e)}"
127
+
128
+ # Step 4: Calculate geometric parameters
129
+ log.append("\n4. Calculating aortic geometry parameters")
130
+ try:
131
+ # Calculate tortuosity (simplified as the ratio of contour length to end-to-end distance)
132
+ contour_length = cv2.arcLength(aorta_contour, closed=True)
133
+
134
+ # Find the two most distant points (approximation for end-to-end distance)
135
+ hull = cv2.convexHull(aorta_contour)
136
+ hull_points = hull.reshape(-1, 2)
137
+ max_dist = 0
138
+ for i in range(len(hull_points)):
139
+ for j in range(i + 1, len(hull_points)):
140
+ dist = np.sqrt(
141
+ (hull_points[i][0] - hull_points[j][0]) ** 2 + (hull_points[i][1] - hull_points[j][1]) ** 2
142
+ )
143
+ max_dist = max(max_dist, dist)
144
+
145
+ if max_dist > 0:
146
+ tortuosity = contour_length / max_dist
147
+ log.append(f" - Aortic tortuosity index: {tortuosity:.2f}")
148
+ else:
149
+ tortuosity = 0
150
+ log.append(" - Warning: Could not calculate tortuosity")
151
+
152
+ # Calculate dilation index (ratio of max diameter to min diameter)
153
+ min_diameter = min(aortic_root_diameter, ascending_diameter)
154
+ max_diameter = max(aortic_root_diameter, ascending_diameter)
155
+
156
+ if min_diameter > 0:
157
+ dilation_index = max_diameter / min_diameter
158
+ log.append(f" - Aortic dilation index: {dilation_index:.2f}")
159
+ else:
160
+ dilation_index = 0
161
+ log.append(" - Warning: Could not calculate dilation index")
162
+
163
+ except Exception as e:
164
+ return f"Error during geometry calculations: {str(e)}"
165
+
166
+ # Step 5: Save results
167
+ log.append("\n5. Saving results")
168
+ try:
169
+ # Create a visual output
170
+ output_image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
171
+ cv2.drawContours(output_image, [aorta_contour], 0, (0, 255, 0), 2)
172
+
173
+ # Mark diameters
174
+ cv2.circle(output_image, (cx, cy), 5, (0, 0, 255), -1)
175
+
176
+ # Save the output image
177
+ output_filename = os.path.join(output_dir, f"aorta_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png")
178
+ cv2.imwrite(output_filename, output_image)
179
+ log.append(f" - Saved annotated image to: {output_filename}")
180
+
181
+ # Save measurements to a text file
182
+ measurements_filename = os.path.join(
183
+ output_dir,
184
+ f"aorta_measurements_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt",
185
+ )
186
+ with open(measurements_filename, "w") as f:
187
+ f.write(f"Aortic Root Diameter: {aortic_root_diameter:.2f} pixels\n")
188
+ f.write(f"Ascending Aorta Diameter: {ascending_diameter:.2f} pixels\n")
189
+ f.write(f"Tortuosity Index: {tortuosity:.2f}\n")
190
+ f.write(f"Dilation Index: {dilation_index:.2f}\n")
191
+ log.append(f" - Saved measurements to: {measurements_filename}")
192
+
193
+ except Exception as e:
194
+ return f"Error during saving results: {str(e)}"
195
+
196
+ # Step 6: Summarize findings
197
+ log.append("\n6. Summary of findings")
198
+ log.append(f" - Aortic root diameter: {aortic_root_diameter:.2f} pixels")
199
+ log.append(f" - Ascending aorta diameter: {ascending_diameter:.2f} pixels")
200
+ log.append(f" - Tortuosity index: {tortuosity:.2f}")
201
+ log.append(f" - Dilation index: {dilation_index:.2f}")
202
+
203
+ return "\n".join(log)
204
+
205
+
206
+ def analyze_atp_luminescence_assay(
207
+ data_file,
208
+ standard_curve_file,
209
+ normalization_method="cell_count",
210
+ normalization_data=None,
211
+ ):
212
+ """Analyze luminescence-based ATP assay data to determine intracellular ATP concentration.
213
+
214
+ Parameters
215
+ ----------
216
+ data_file : str
217
+ Path to CSV file containing luminescence readings from samples.
218
+ Expected format: columns for 'Sample_ID' and 'Luminescence_Value'.
219
+
220
+ standard_curve_file : str
221
+ Path to CSV file containing standard curve data.
222
+ Expected format: columns for 'ATP_Concentration' (in nM) and 'Luminescence_Value'.
223
+
224
+ normalization_method : str, optional
225
+ Method used to normalize ATP values, either 'cell_count' or 'protein_content'.
226
+ Default is 'cell_count'.
227
+
228
+ normalization_data : str or dict, optional
229
+ Either path to CSV file with normalization data or dictionary with sample IDs as keys.
230
+ For 'cell_count': values should be cell counts per sample.
231
+ For 'protein_content': values should be protein concentration (μg/mL).
232
+
233
+ Returns
234
+ -------
235
+ str
236
+ Research log summarizing the ATP content measurement process and results.
237
+
238
+ """
239
+ from datetime import datetime
240
+
241
+ import numpy as np
242
+ import pandas as pd
243
+
244
+ log = []
245
+ log.append(f"ATP Content Measurement Analysis - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
246
+ log.append("=" * 50)
247
+
248
+ # Step 1: Load the raw luminescence data
249
+ log.append("Step 1: Loading raw luminescence data")
250
+ try:
251
+ sample_data = pd.read_csv(data_file)
252
+ log.append(f"Successfully loaded data from {data_file}")
253
+ log.append(f"Number of samples: {len(sample_data)}")
254
+ except Exception as e:
255
+ log.append(f"Error loading sample data: {str(e)}")
256
+ return "\n".join(log)
257
+
258
+ # Step 2: Load the standard curve data
259
+ log.append("\nStep 2: Loading ATP standard curve data")
260
+ try:
261
+ std_curve_data = pd.read_csv(standard_curve_file)
262
+ log.append(f"Successfully loaded standard curve from {standard_curve_file}")
263
+ except Exception as e:
264
+ log.append(f"Error loading standard curve data: {str(e)}")
265
+ return "\n".join(log)
266
+
267
+ # Step 3: Generate standard curve equation (linear regression)
268
+ log.append("\nStep 3: Generating ATP standard curve")
269
+ try:
270
+ x = std_curve_data["Luminescence_Value"]
271
+ y = std_curve_data["ATP_Concentration"]
272
+ slope, intercept = np.polyfit(x, y, 1)
273
+ log.append(f"Standard curve equation: ATP_Concentration = {slope:.6f} × Luminescence + {intercept:.6f}")
274
+ except Exception as e:
275
+ log.append(f"Error generating standard curve: {str(e)}")
276
+ return "\n".join(log)
277
+
278
+ # Step 4: Calculate ATP concentrations for samples
279
+ log.append("\nStep 4: Calculating ATP concentrations for samples")
280
+ try:
281
+ sample_data["ATP_Concentration_nM"] = slope * sample_data["Luminescence_Value"] + intercept
282
+ log.append("ATP concentrations calculated for all samples")
283
+ except Exception as e:
284
+ log.append(f"Error calculating ATP concentrations: {str(e)}")
285
+ return "\n".join(log)
286
+
287
+ # Step 5: Normalize ATP concentrations
288
+ log.append(f"\nStep 5: Normalizing ATP concentrations by {normalization_method}")
289
+
290
+ # Load normalization data
291
+ if normalization_data:
292
+ try:
293
+ if isinstance(normalization_data, str):
294
+ norm_data = pd.read_csv(normalization_data)
295
+ norm_dict = dict(zip(norm_data["Sample_ID"], norm_data[normalization_method], strict=False))
296
+ else:
297
+ norm_dict = normalization_data
298
+
299
+ # Apply normalization
300
+ for idx, row in sample_data.iterrows():
301
+ sample_id = row["Sample_ID"]
302
+ if sample_id in norm_dict:
303
+ if normalization_method == "cell_count":
304
+ sample_data.at[idx, "ATP_pmol_per_million_cells"] = (
305
+ row["ATP_Concentration_nM"] / norm_dict[sample_id] * 1000
306
+ )
307
+ elif normalization_method == "protein_content":
308
+ sample_data.at[idx, "ATP_nmol_per_mg_protein"] = (
309
+ row["ATP_Concentration_nM"] / norm_dict[sample_id]
310
+ )
311
+
312
+ log.append(f"Successfully normalized ATP concentrations by {normalization_method}")
313
+ except Exception as e:
314
+ log.append(f"Error during normalization: {str(e)}")
315
+ log.append("Continuing with unnormalized ATP concentrations")
316
+ else:
317
+ log.append("No normalization data provided. Reporting unnormalized ATP concentrations.")
318
+
319
+ # Step 6: Save results
320
+ output_file = "atp_measurement_results.csv"
321
+ try:
322
+ sample_data.to_csv(output_file, index=False)
323
+ log.append(f"\nStep 6: Results saved to {output_file}")
324
+ except Exception as e:
325
+ log.append(f"\nStep 6: Error saving results: {str(e)}")
326
+
327
+ # Step 7: Summary statistics
328
+ log.append("\nStep 7: Summary of ATP measurement results")
329
+ try:
330
+ log.append("ATP Concentration Summary (nM):")
331
+ log.append(f" Mean: {sample_data['ATP_Concentration_nM'].mean():.2f}")
332
+ log.append(f" Median: {sample_data['ATP_Concentration_nM'].median():.2f}")
333
+ log.append(f" Min: {sample_data['ATP_Concentration_nM'].min():.2f}")
334
+ log.append(f" Max: {sample_data['ATP_Concentration_nM'].max():.2f}")
335
+
336
+ if normalization_method == "cell_count" and "ATP_pmol_per_million_cells" in sample_data.columns:
337
+ log.append("\nATP per Million Cells (pmol):")
338
+ log.append(f" Mean: {sample_data['ATP_pmol_per_million_cells'].mean():.2f}")
339
+ log.append(f" Median: {sample_data['ATP_pmol_per_million_cells'].median():.2f}")
340
+ elif normalization_method == "protein_content" and "ATP_nmol_per_mg_protein" in sample_data.columns:
341
+ log.append("\nATP per mg Protein (nmol):")
342
+ log.append(f" Mean: {sample_data['ATP_nmol_per_mg_protein'].mean():.2f}")
343
+ log.append(f" Median: {sample_data['ATP_nmol_per_mg_protein'].median():.2f}")
344
+ except Exception as e:
345
+ log.append(f"Error generating summary statistics: {str(e)}")
346
+
347
+ log.append("\nATP Content Measurement Analysis Complete")
348
+
349
+ return "\n".join(log)
350
+
351
+
352
+ def analyze_thrombus_histology(image_path, output_dir="./output"):
353
+ """Analyze histological images of thrombus samples stained with H&E to identify and quantify
354
+ different thrombus components (fresh, cellular lysis, endothelialization, fibroblastic reaction).
355
+
356
+ Parameters
357
+ ----------
358
+ image_path : str
359
+ Path to the histological image of thrombus sample stained with H&E
360
+ output_dir : str, optional
361
+ Directory to save output files (default: "./output")
362
+
363
+ Returns
364
+ -------
365
+ str
366
+ Research log summarizing the analysis steps and results
367
+
368
+ """
369
+ import os
370
+ from datetime import datetime
371
+
372
+ import cv2
373
+ import numpy as np
374
+ from skimage import color
375
+
376
+ # Create output directory if it doesn't exist
377
+ if not os.path.exists(output_dir):
378
+ os.makedirs(output_dir)
379
+
380
+ # Initialize research log
381
+ log = f"# Thrombus Component Analysis - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
382
+ log += f"Analyzing image: {image_path}\n\n"
383
+
384
+ # Step 1: Load and preprocess the image
385
+ log += "## Step 1: Image Loading and Preprocessing\n"
386
+ original_image = cv2.imread(image_path)
387
+ if original_image is None:
388
+ return f"Error: Could not load image from {image_path}"
389
+
390
+ # Convert to RGB (from BGR)
391
+ rgb_image = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
392
+ log += f"- Loaded image with dimensions: {rgb_image.shape[1]}x{rgb_image.shape[0]} pixels\n"
393
+
394
+ # Convert to LAB color space for better color segmentation
395
+ lab_image = color.rgb2lab(rgb_image)
396
+ log += "- Converted image to LAB color space for improved color-based segmentation\n\n"
397
+
398
+ # Step 2: Segment thrombus components based on color characteristics
399
+ log += "## Step 2: Thrombus Component Segmentation\n"
400
+
401
+ # Create masks for different components based on color thresholds
402
+ # These thresholds are approximate and may need adjustment for specific staining protocols
403
+
404
+ # Fresh thrombus: typically red (erythrocytes) with blue nuclei (leukocytes)
405
+ # Usually appears as bright red/pink areas with scattered dark blue/purple nuclei
406
+ fresh_mask = (
407
+ (lab_image[:, :, 0] > 50) # Lightness
408
+ & (lab_image[:, :, 1] > 15) # a* channel (red component)
409
+ )
410
+
411
+ # Cellular lysis: degraded cellular components, less intense staining
412
+ lysis_mask = (
413
+ (lab_image[:, :, 0] > 60) # Lightness
414
+ & (lab_image[:, :, 1] < 15) # Less red
415
+ & (lab_image[:, :, 1] > -5)
416
+ & (lab_image[:, :, 2] < 10) # Less blue
417
+ )
418
+
419
+ # Endothelialization: endothelial cells lining the thrombus
420
+ # Typically appears as thin, organized cellular layers
421
+ endothel_mask = (
422
+ (lab_image[:, :, 0] > 40) & (lab_image[:, :, 0] < 70) & (lab_image[:, :, 2] < -5) # More blue component
423
+ )
424
+
425
+ # Fibroblastic reaction: fibroblasts and collagen deposition
426
+ # Usually appears as pale pink/purple fibrous structures
427
+ fibro_mask = (
428
+ (lab_image[:, :, 0] > 70) # Lighter areas
429
+ & (lab_image[:, :, 1] < 5) # Less red
430
+ & (lab_image[:, :, 1] > -10)
431
+ & (lab_image[:, :, 2] > 0) # Less blue
432
+ )
433
+
434
+ log += "- Created masks for each thrombus component based on color characteristics\n"
435
+
436
+ # Step 3: Quantify the components
437
+ log += "\n## Step 3: Component Quantification\n"
438
+
439
+ # Calculate total pixel count (excluding background)
440
+ total_pixels = fresh_mask.sum() + lysis_mask.sum() + endothel_mask.sum() + fibro_mask.sum()
441
+
442
+ # Calculate percentages
443
+ if total_pixels > 0:
444
+ fresh_percent = (fresh_mask.sum() / total_pixels) * 100
445
+ lysis_percent = (lysis_mask.sum() / total_pixels) * 100
446
+ endothel_percent = (endothel_mask.sum() / total_pixels) * 100
447
+ fibro_percent = (fibro_mask.sum() / total_pixels) * 100
448
+ else:
449
+ fresh_percent = lysis_percent = endothel_percent = fibro_percent = 0
450
+
451
+ log += f"- Fresh thrombus: {fresh_percent:.2f}%\n"
452
+ log += f"- Cellular lysis: {lysis_percent:.2f}%\n"
453
+ log += f"- Endothelialization: {endothel_percent:.2f}%\n"
454
+ log += f"- Fibroblastic reaction: {fibro_percent:.2f}%\n\n"
455
+
456
+ # Step 4: Visualize the results
457
+ log += "## Step 4: Visualization\n"
458
+
459
+ # Create a visualization image
460
+ visualization = np.zeros_like(rgb_image)
461
+
462
+ # Assign colors to different components
463
+ visualization[fresh_mask] = [255, 0, 0] # Red for fresh thrombus
464
+ visualization[lysis_mask] = [0, 255, 0] # Green for cellular lysis
465
+ visualization[endothel_mask] = [0, 0, 255] # Blue for endothelialization
466
+ visualization[fibro_mask] = [255, 255, 0] # Yellow for fibroblastic reaction
467
+
468
+ # Save the visualization
469
+ output_filename = os.path.join(output_dir, f"thrombus_components_{os.path.basename(image_path)}")
470
+ cv2.imwrite(output_filename, cv2.cvtColor(visualization, cv2.COLOR_RGB2BGR))
471
+
472
+ log += f"- Visualization saved as: {output_filename}\n"
473
+ log += "- Color legend:\n"
474
+ log += " - Red: Fresh thrombus\n"
475
+ log += " - Green: Cellular lysis\n"
476
+ log += " - Blue: Endothelialization\n"
477
+ log += " - Yellow: Fibroblastic reaction\n\n"
478
+
479
+ # Step 5: Summary
480
+ log += "## Summary\n"
481
+ log += "The histological analysis of the thrombus sample revealed the following composition:\n\n"
482
+ log += f"1. Fresh thrombus: {fresh_percent:.2f}%\n"
483
+ log += f"2. Cellular lysis: {lysis_percent:.2f}%\n"
484
+ log += f"3. Endothelialization: {endothel_percent:.2f}%\n"
485
+ log += f"4. Fibroblastic reaction: {fibro_percent:.2f}%\n\n"
486
+
487
+ # Save results to CSV
488
+ import csv
489
+
490
+ csv_filename = os.path.join(
491
+ output_dir,
492
+ f"thrombus_analysis_{os.path.basename(image_path).split('.')[0]}.csv",
493
+ )
494
+ with open(csv_filename, "w", newline="") as csvfile:
495
+ writer = csv.writer(csvfile)
496
+ writer.writerow(["Component", "Percentage"])
497
+ writer.writerow(["Fresh thrombus", f"{fresh_percent:.2f}"])
498
+ writer.writerow(["Cellular lysis", f"{lysis_percent:.2f}"])
499
+ writer.writerow(["Endothelialization", f"{endothel_percent:.2f}"])
500
+ writer.writerow(["Fibroblastic reaction", f"{fibro_percent:.2f}"])
501
+
502
+ log += f"Quantitative results saved to: {csv_filename}\n"
503
+
504
+ return log
505
+
506
+
507
+ def analyze_intracellular_calcium_with_rhod2(
508
+ background_image_path, control_image_path, sample_image_path, output_dir="./output"
509
+ ):
510
+ """Analyzes intracellular calcium concentration using Rhod-2 fluorescent indicator from microscopy images.
511
+
512
+ Parameters
513
+ ----------
514
+ background_image_path : str
515
+ Path to the background image (no cells, just media)
516
+ control_image_path : str
517
+ Path to the control image (cells without calcium stimulus)
518
+ sample_image_path : str
519
+ Path to the sample image (cells with calcium stimulus)
520
+ output_dir : str, optional
521
+ Directory to save output files, default is "./output"
522
+
523
+ Returns
524
+ -------
525
+ str
526
+ Research log summarizing the analysis steps and results
527
+
528
+ """
529
+ import os
530
+ from datetime import datetime
531
+
532
+ import cv2
533
+ import matplotlib.pyplot as plt
534
+ import numpy as np
535
+
536
+ # Create output directory if it doesn't exist
537
+ os.makedirs(output_dir, exist_ok=True)
538
+
539
+ # Step 1: Load images
540
+ background = cv2.imread(background_image_path, cv2.IMREAD_GRAYSCALE).astype(float)
541
+ control = cv2.imread(control_image_path, cv2.IMREAD_GRAYSCALE).astype(float)
542
+ sample = cv2.imread(sample_image_path, cv2.IMREAD_GRAYSCALE).astype(float)
543
+
544
+ # Step 2: Background subtraction
545
+ control_corrected = np.maximum(control - background, 0)
546
+ sample_corrected = np.maximum(sample - background, 0)
547
+
548
+ # Step 3: Calculate fluorescence intensity (mean values)
549
+ control_intensity = np.mean(control_corrected)
550
+ sample_intensity = np.mean(sample_corrected)
551
+
552
+ # Step 4: Convert to calcium concentration using a standard calibration equation
553
+ # [Ca2+] = Kd * (F - Fmin) / (Fmax - F)
554
+ # For Rhod-2, Kd is approximately 570 nM
555
+ # We'll use control as Fmin and estimate Fmax as 2.5x the sample value (typical for max calcium response)
556
+ kd = 570 # nM
557
+ f_min = control_intensity
558
+ f_max = 2.5 * sample_intensity # Estimated maximum
559
+ f = sample_intensity
560
+
561
+ # Guard against division by zero
562
+ calcium_concentration = float("inf") if f_max == f else kd * (f - f_min) / (f_max - f)
563
+
564
+ # Step 5: Generate heatmap visualization of calcium concentration
565
+ plt.figure(figsize=(10, 8))
566
+ calcium_map = (sample_corrected - control_corrected) / (f_max - control_corrected + 1e-10) * kd
567
+ plt.imshow(calcium_map, cmap="hot")
568
+ plt.colorbar(label="[Ca²⁺] (nM)")
569
+ plt.title("Intracellular Calcium Concentration Map")
570
+
571
+ # Save the visualization
572
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
573
+ result_filename = f"{output_dir}/calcium_map_{timestamp}.png"
574
+ plt.savefig(result_filename)
575
+ plt.close()
576
+
577
+ # Generate research log
578
+ log = f"""
579
+ Intracellular Calcium Imaging Analysis Log - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
580
+ ------------------------------------------------------------
581
+ 1. Loaded microscopy images:
582
+ - Background image: {background_image_path}
583
+ - Control image: {control_image_path}
584
+ - Sample image: {sample_image_path}
585
+
586
+ 2. Performed background subtraction to correct for autofluorescence
587
+
588
+ 3. Calculated fluorescence intensity:
589
+ - Control intensity: {control_intensity:.2f} a.u.
590
+ - Sample intensity: {sample_intensity:.2f} a.u.
591
+
592
+ 4. Converted fluorescence to calcium concentration using Rhod-2 calibration:
593
+ - Kd value: {kd} nM
594
+ - Mean intracellular [Ca²⁺]: {calcium_concentration:.2f} nM
595
+
596
+ 5. Generated calcium concentration heatmap saved to:
597
+ - {result_filename}
598
+
599
+ Analysis complete. The sample shows {calcium_concentration:.2f} nM intracellular calcium concentration.
600
+ """
601
+
602
+ return log
603
+
604
+
605
+ def quantify_corneal_nerve_fibers(image_path, marker_type, output_dir="./output", threshold_method="otsu"):
606
+ """Quantify the volume/density of immunofluorescence-labeled corneal nerve fibers.
607
+
608
+ Parameters
609
+ ----------
610
+ image_path : str
611
+ Path to the immunofluorescence microscopy image file
612
+ marker_type : str
613
+ Type of nerve fiber marker (e.g., 'βIII-tubulin', 'SP', 'L1CAM')
614
+ output_dir : str, optional
615
+ Directory to save output files (default: './output')
616
+ threshold_method : str, optional
617
+ Method for thresholding ('otsu', 'adaptive', 'manual'), default is 'otsu'
618
+
619
+ Returns
620
+ -------
621
+ str
622
+ Research log summarizing the analysis steps and results
623
+
624
+ """
625
+ import os
626
+ from datetime import datetime
627
+
628
+ import numpy as np
629
+ from skimage import filters, io, measure, morphology, util
630
+ from skimage.color import rgb2gray
631
+
632
+ # Create output directory if it doesn't exist
633
+ os.makedirs(output_dir, exist_ok=True)
634
+
635
+ # Generate timestamp for unique filenames
636
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
637
+ base_filename = f"{marker_type.replace(' ', '_')}_{timestamp}"
638
+
639
+ # Step 1: Load the image
640
+ try:
641
+ image = io.imread(image_path)
642
+ # Convert to grayscale if RGB
643
+ if len(image.shape) > 2:
644
+ image = rgb2gray(image)
645
+ except Exception as e:
646
+ return f"Error loading image: {str(e)}"
647
+
648
+ # Step 2: Preprocess the image
649
+ # Normalize to 0-1 range
650
+ image_normalized = image.astype(float)
651
+ image_normalized = (image_normalized - image_normalized.min()) / (image_normalized.max() - image_normalized.min())
652
+
653
+ # Step 3: Segment the nerve fibers
654
+ if threshold_method == "otsu":
655
+ threshold_value = filters.threshold_otsu(image_normalized)
656
+ elif threshold_method == "adaptive":
657
+ threshold_value = filters.threshold_local(image_normalized, block_size=35)
658
+ else: # manual
659
+ threshold_value = 0.5 # Default manual threshold
660
+
661
+ # Create binary mask
662
+ binary_mask = image_normalized > threshold_value
663
+
664
+ # Step 4: Clean up with morphological operations
665
+ # Remove small objects
666
+ cleaned_mask = morphology.remove_small_objects(binary_mask, min_size=50)
667
+ # Fill holes
668
+ cleaned_mask = morphology.closing(cleaned_mask, morphology.disk(2))
669
+
670
+ # Step 5: Quantify the nerve fibers
671
+ # Calculate total area of nerve fibers (in pixels)
672
+ fiber_area = np.sum(cleaned_mask)
673
+ # Calculate total image area
674
+ total_area = cleaned_mask.size
675
+ # Calculate fiber density (percentage of area covered by fibers)
676
+ fiber_density = (fiber_area / total_area) * 100
677
+
678
+ # Calculate properties of individual fiber segments
679
+ labeled_fibers = measure.label(cleaned_mask)
680
+ fiber_props = measure.regionprops(labeled_fibers)
681
+
682
+ # Calculate average fiber length and width
683
+ total_length = 0
684
+ total_width = 0
685
+ fiber_count = len(fiber_props)
686
+
687
+ if fiber_count > 0:
688
+ for prop in fiber_props:
689
+ # Approximating length as major axis length
690
+ total_length += prop.major_axis_length
691
+ # Approximating width as minor axis length
692
+ total_width += prop.minor_axis_length
693
+
694
+ avg_length = total_length / fiber_count
695
+ avg_width = total_width / fiber_count
696
+ else:
697
+ avg_length = 0
698
+ avg_width = 0
699
+
700
+ # Step 6: Save results
701
+ # Save segmented image
702
+ segmented_image_path = os.path.join(output_dir, f"{base_filename}_segmented.png")
703
+ io.imsave(segmented_image_path, util.img_as_ubyte(cleaned_mask))
704
+
705
+ # Save measurements to CSV
706
+ measurements_path = os.path.join(output_dir, f"{base_filename}_measurements.csv")
707
+ with open(measurements_path, "w") as f:
708
+ f.write("Metric,Value\n")
709
+ f.write(f"Marker Type,{marker_type}\n")
710
+ f.write(f"Fiber Area (pixels),{fiber_area}\n")
711
+ f.write(f"Total Image Area (pixels),{total_area}\n")
712
+ f.write(f"Fiber Density (%),{fiber_density:.2f}\n")
713
+ f.write(f"Fiber Count,{fiber_count}\n")
714
+ f.write(f"Average Fiber Length (pixels),{avg_length:.2f}\n")
715
+ f.write(f"Average Fiber Width (pixels),{avg_width:.2f}\n")
716
+
717
+ # Step 7: Generate research log
718
+ log = f"""
719
+ Research Log: Quantification of {marker_type} Labeled Corneal Nerve Fibers
720
+ Date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
721
+ Image: {os.path.basename(image_path)}
722
+
723
+ Analysis Steps:
724
+ 1. Loaded and preprocessed the immunofluorescence microscopy image
725
+ 2. Applied {threshold_method} thresholding for nerve fiber segmentation
726
+ 3. Performed morphological operations to clean the segmentation
727
+ 4. Quantified nerve fiber metrics
728
+
729
+ Results:
730
+ - Fiber Area: {fiber_area} pixels
731
+ - Total Image Area: {total_area} pixels
732
+ - Fiber Density: {fiber_density:.2f}%
733
+ - Number of Fiber Segments: {fiber_count}
734
+ - Average Fiber Length: {avg_length:.2f} pixels
735
+ - Average Fiber Width: {avg_width:.2f} pixels
736
+
737
+ Output Files:
738
+ - Segmented Image: {os.path.basename(segmented_image_path)}
739
+ - Measurements: {os.path.basename(measurements_path)}
740
+ """
741
+
742
+ return log
743
+
744
+
745
+ def segment_and_quantify_cells_in_multiplexed_images(
746
+ image_path, markers_list, nuclear_channel_index=0, output_dir="./output"
747
+ ):
748
+ """Segment cells and quantify protein expression levels from multichannel tissue images.
749
+
750
+ Parameters
751
+ ----------
752
+ image_path : str
753
+ Path to the multichannel image file (tiff stack or similar format)
754
+ markers_list : list of str
755
+ List of marker names corresponding to each channel in the image
756
+ nuclear_channel_index : int, optional
757
+ Index of the nuclear marker channel (default: 0, typically DAPI)
758
+ output_dir : str, optional
759
+ Directory to save output files (default: "./output")
760
+
761
+ Returns
762
+ -------
763
+ str
764
+ Research log summarizing the steps performed and output file locations
765
+
766
+ """
767
+ import os
768
+ from datetime import datetime
769
+
770
+ import numpy as np
771
+ import pandas as pd
772
+ from scipy import ndimage # Import ndimage for distance transform
773
+ from skimage import filters, io, measure, morphology, segmentation
774
+
775
+ # Initialize research log
776
+ log = []
777
+ log.append(
778
+ f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Starting cell segmentation and protein quantification"
779
+ )
780
+
781
+ # Create output directory if it doesn't exist
782
+ if not os.path.exists(output_dir):
783
+ os.makedirs(output_dir)
784
+ log.append(f"Created output directory: {output_dir}")
785
+
786
+ # Load the multichannel image
787
+ try:
788
+ log.append(f"Loading multichannel image from {image_path}")
789
+ image = io.imread(image_path)
790
+ log.append(f"Image loaded successfully. Shape: {image.shape}")
791
+
792
+ # Validate image dimensions
793
+ if len(image.shape) < 3:
794
+ return (
795
+ "Error: Image should be multichannel with shape (channels, height, width) or (height, width, channels)"
796
+ )
797
+
798
+ # Determine image format
799
+ if image.shape[0] == len(markers_list):
800
+ # Format is (channels, height, width)
801
+ num_channels, height, width = image.shape
802
+ # Extract nuclear channel
803
+ nuclear_channel = image[nuclear_channel_index]
804
+ elif image.shape[-1] == len(markers_list):
805
+ # Format is (height, width, channels)
806
+ height, width, num_channels = image.shape
807
+ # Extract nuclear channel
808
+ nuclear_channel = image[:, :, nuclear_channel_index]
809
+ else:
810
+ return f"Error: Number of channels in image ({image.shape}) doesn't match markers list length ({len(markers_list)})"
811
+
812
+ log.append(f"Processing image with {num_channels} channels, dimensions: {height}x{width}")
813
+
814
+ except Exception as e:
815
+ return f"Error loading image: {str(e)}"
816
+
817
+ # Segment nuclei
818
+ log.append("Performing nuclear segmentation")
819
+ # Apply threshold to nuclear channel
820
+ threshold = filters.threshold_otsu(nuclear_channel)
821
+ binary_nuclei = nuclear_channel > threshold
822
+
823
+ # Clean up binary image
824
+ binary_nuclei = morphology.remove_small_objects(binary_nuclei, min_size=50)
825
+ binary_nuclei = morphology.binary_closing(binary_nuclei, morphology.disk(2))
826
+
827
+ # Label nuclei
828
+ labeled_nuclei = measure.label(binary_nuclei)
829
+ log.append(f"Identified {np.max(labeled_nuclei)} potential nuclei")
830
+
831
+ # Create cell masks by expanding nuclei
832
+ log.append("Expanding nuclear masks to approximate cell boundaries")
833
+ cell_masks = segmentation.watershed(
834
+ -ndimage.distance_transform_edt(~binary_nuclei), # Use ndimage for distance transform
835
+ labeled_nuclei,
836
+ mask=morphology.binary_dilation(binary_nuclei, morphology.disk(10)),
837
+ )
838
+
839
+ # Get region properties for nuclei
840
+ regions = measure.regionprops(cell_masks)
841
+ log.append(f"Segmented {len(regions)} cells")
842
+
843
+ # Create a dataframe to store cell features
844
+ log.append("Extracting features and quantifying marker intensities")
845
+ cell_data = []
846
+
847
+ for i, region in enumerate(regions):
848
+ cell_features = {
849
+ "cell_id": i + 1,
850
+ "centroid_y": region.centroid[0],
851
+ "centroid_x": region.centroid[1],
852
+ "area": region.area,
853
+ }
854
+
855
+ # Extract intensity for each marker
856
+ for channel_idx, marker_name in enumerate(markers_list):
857
+ if image.shape[0] == len(markers_list):
858
+ # Format is (channels, height, width)
859
+ marker_image = image[channel_idx]
860
+ else:
861
+ # Format is (height, width, channels)
862
+ marker_image = image[:, :, channel_idx]
863
+
864
+ # Create mask for current cell
865
+ cell_mask = cell_masks == region.label
866
+
867
+ # Calculate mean intensity within the cell mask
868
+ mean_intensity = np.mean(marker_image[cell_mask])
869
+ cell_features[f"{marker_name}_mean_intensity"] = mean_intensity
870
+
871
+ cell_data.append(cell_features)
872
+
873
+ # Create dataframe
874
+ cell_df = pd.DataFrame(cell_data)
875
+
876
+ # Save results
877
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
878
+ results_file = os.path.join(output_dir, f"cell_features_{timestamp}.csv")
879
+ cell_df.to_csv(results_file, index=False)
880
+ log.append(f"Saved spatial feature table to {results_file}")
881
+
882
+ # Save segmentation mask for visualization
883
+ mask_file = os.path.join(output_dir, f"cell_segmentation_mask_{timestamp}.tiff")
884
+ io.imsave(mask_file, cell_masks.astype(np.uint16))
885
+ log.append(f"Saved cell segmentation mask to {mask_file}")
886
+
887
+ # Summarize results
888
+ log.append(f"Analysis complete. Processed {num_channels} markers across {len(regions)} cells.")
889
+ log.append("Output files:")
890
+ log.append(f" - Spatial feature table: {results_file}")
891
+ log.append(f" - Cell segmentation mask: {mask_file}")
892
+
893
+ return "\n".join(log)
894
+
895
+
896
+ def analyze_bone_microct_morphometry(input_file_path, output_dir="./results", threshold_value=None):
897
+ """Analyze bone microarchitecture parameters from 3D micro-CT images.
898
+
899
+ Performs quantitative analysis of bone microstructure to calculate bone mineral density (BMD),
900
+ bone volume (BV), trabecular number (Tb.N), trabecular thickness (Tb.Th), and
901
+ trabecular separation (Tb.S) from micro-CT data.
902
+
903
+ Parameters
904
+ ----------
905
+ input_file_path : str
906
+ Path to the micro-CT scan data file (TIFF stack or similar 3D format)
907
+ output_dir : str, optional
908
+ Directory to save output files, default is "./results"
909
+ threshold_value : float, optional
910
+ Threshold value for bone segmentation. If None, Otsu's method will be used
911
+
912
+ Returns
913
+ -------
914
+ str
915
+ Research log summarizing the analysis steps and results
916
+
917
+ """
918
+ import json
919
+ import os
920
+ from datetime import datetime
921
+
922
+ import numpy as np
923
+ from scipy import ndimage
924
+ from skimage import filters, io
925
+
926
+ # Create output directory if it doesn't exist
927
+ os.makedirs(output_dir, exist_ok=True)
928
+
929
+ # Start research log
930
+ log = [
931
+ "# Micro-CT Bone Morphometry Analysis",
932
+ f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
933
+ f"Input file: {input_file_path}",
934
+ ]
935
+
936
+ # Step 1: Load the micro-CT data
937
+ log.append("\n## 1. Data Loading")
938
+ try:
939
+ log.append(f"Loading 3D micro-CT data from {input_file_path}")
940
+ # Load the 3D image data
941
+ image_data = io.imread(input_file_path)
942
+ if image_data.ndim < 3:
943
+ log.append("Warning: Input appears to be 2D. Converting to 3D.")
944
+ image_data = np.expand_dims(image_data, axis=0)
945
+
946
+ log.append(f"Data loaded successfully. Dimensions: {image_data.shape}")
947
+ voxel_count = image_data.size
948
+ log.append(f"Total voxels: {voxel_count}")
949
+ except Exception as e:
950
+ error_msg = f"Error loading data: {str(e)}"
951
+ log.append(error_msg)
952
+ return "\n".join(log)
953
+
954
+ # Step 2: Preprocess the data
955
+ log.append("\n## 2. Preprocessing")
956
+ log.append("Applying median filter to reduce noise")
957
+ filtered_data = ndimage.median_filter(image_data, size=2)
958
+
959
+ # Step 3: Segmentation of bone tissue
960
+ log.append("\n## 3. Bone Segmentation")
961
+ if threshold_value is None:
962
+ log.append("Calculating optimal threshold using Otsu's method")
963
+ threshold_value = filters.threshold_otsu(filtered_data)
964
+
965
+ log.append(f"Segmenting bone using threshold value: {threshold_value}")
966
+ binary_image = filtered_data > threshold_value
967
+
968
+ # Step 4: Calculate bone morphometry parameters
969
+ log.append("\n## 4. Morphometry Analysis")
970
+
971
+ # 4.1 Calculate BMD (simplified as mean intensity in bone regions)
972
+ bmd = np.mean(image_data[binary_image])
973
+ log.append(f"Bone Mineral Density (BMD): {bmd:.2f} arbitrary units")
974
+
975
+ # 4.2 Calculate BV (bone volume)
976
+ bone_volume = np.sum(binary_image)
977
+ total_volume = binary_image.size
978
+ bv_tv_ratio = bone_volume / total_volume
979
+ log.append(f"Bone Volume (BV): {bone_volume} voxels")
980
+ log.append(f"BV/TV Ratio: {bv_tv_ratio:.4f}")
981
+
982
+ # 4.3 Calculate trabecular thickness (Tb.Th)
983
+ # Using distance transform method
984
+ log.append("Calculating trabecular thickness (Tb.Th)")
985
+ distance_map = ndimage.distance_transform_edt(binary_image)
986
+ tb_th_mean = np.mean(distance_map[binary_image]) * 2 # Multiply by 2 for diameter
987
+ log.append(f"Trabecular Thickness (Tb.Th): {tb_th_mean:.2f} voxels")
988
+
989
+ # 4.4 Calculate trabecular separation (Tb.S)
990
+ log.append("Calculating trabecular separation (Tb.S)")
991
+ distance_map_inv = ndimage.distance_transform_edt(~binary_image)
992
+ tb_s_mean = np.mean(distance_map_inv[~binary_image])
993
+ log.append(f"Trabecular Separation (Tb.S): {tb_s_mean:.2f} voxels")
994
+
995
+ # 4.5 Calculate trabecular number (Tb.N)
996
+ # Simplified calculation: Tb.N = (BV/TV) / Tb.Th
997
+ log.append("Calculating trabecular number (Tb.N)")
998
+ tb_n = bv_tv_ratio / tb_th_mean if tb_th_mean > 0 else 0
999
+ log.append(f"Trabecular Number (Tb.N): {tb_n:.4f} 1/voxel")
1000
+
1001
+ # Step 5: Save results
1002
+ log.append("\n## 5. Saving Results")
1003
+
1004
+ # Save binary segmentation as a sample slice
1005
+ middle_slice = binary_image.shape[0] // 2
1006
+ segmentation_file = os.path.join(output_dir, "bone_segmentation_slice.tif")
1007
+ io.imsave(segmentation_file, binary_image[middle_slice].astype(np.uint8) * 255)
1008
+ log.append(f"Sample segmentation slice saved to: {segmentation_file}")
1009
+
1010
+ # Save numerical results as JSON
1011
+ results = {
1012
+ "BMD": float(bmd),
1013
+ "BV": int(bone_volume),
1014
+ "BV/TV": float(bv_tv_ratio),
1015
+ "Tb.Th": float(tb_th_mean),
1016
+ "Tb.S": float(tb_s_mean),
1017
+ "Tb.N": float(tb_n),
1018
+ }
1019
+
1020
+ results_file = os.path.join(output_dir, "bone_morphometry_results.json")
1021
+ with open(results_file, "w") as f:
1022
+ json.dump(results, f, indent=4)
1023
+ log.append(f"Numerical results saved to: {results_file}")
1024
+
1025
+ # Summary of results
1026
+ log.append("\n## 6. Summary of Bone Morphometry Results")
1027
+ log.append(f"- Bone Mineral Density (BMD): {bmd:.2f} arbitrary units")
1028
+ log.append(f"- Bone Volume (BV): {bone_volume} voxels")
1029
+ log.append(f"- BV/TV Ratio: {bv_tv_ratio:.4f}")
1030
+ log.append(f"- Trabecular Thickness (Tb.Th): {tb_th_mean:.2f} voxels")
1031
+ log.append(f"- Trabecular Separation (Tb.S): {tb_s_mean:.2f} voxels")
1032
+ log.append(f"- Trabecular Number (Tb.N): {tb_n:.4f} 1/voxel")
1033
+
1034
+ return "\n".join(log)
Biomni/biomni/tool/protocols/thermofisher/Cell Freezing Protocols _ Thermo Fisher Scientific - US.txt ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PROTOCOL: Cell Freezing Protocols
2
+ SOURCE: https://www.thermofisher.com/us/en/home/references/gibco-cell-culture-basics/cell-culture-protocols/freezing-cells.html
3
+ ================================================================================
4
+
5
+ OVERVIEW
6
+ ================================================================================
7
+
8
+ After subculturing your adherent or suspension cells, you may need to freeze any cells not required for immediate experiments. This cell freezing protocol outlines the necessary preparations and materials to order before beginning the freezing process. It also provides step-by-step instructions that you can follow during the procedure or refer to for future use.
9
+
10
+ Cryopreservation of cells, or the freezing of cells, is a technique used to store living cells and tissues at extremely low temperatures to preserve their structure over an indefinite period. By lowering the temperature to below –130°C, extracellular ice forms, decreasing kinetic and molecular activity within the cells and slowing biological aging.
11
+
12
+ Why freeze your cells?
13
+
14
+ • Freezing cells is crucial for maintaining an adequate cell stock
15
+ • Cell lines are valuable resources whose replacement is both expensive and time-consuming
16
+ • Cryopreservation of finite cell lines prevents aging and transformation
17
+ • Freezing cells in continuous culture can prevent:
18
+ - genetic drift
19
+ - senescence
20
+ - microbial contamination
21
+ - equipment failure
22
+
23
+ When a small surplus of cells becomes available from subculturing, a highly effective method is to freeze them as a seed stock, keeping them protected and not available for general laboratory use. Working stocks can be prepared and replenished from these frozen seed stocks. If the seed stocks become depleted, cryopreserved working stocks can serve as a source for preparing fresh seed stock with minimal generation increase from the initial freezing.
24
+
25
+ MATERIALS REQUIRED
26
+ ================================================================================
27
+
28
+ We suggest using the following materials for the cell freezing protocol:
29
+
30
+ • Log-phase cultured cells
31
+ • Complete growth medium: basal medium, serum, and supplements pre-warmed to 37°C
32
+ • Cryoprotective agent such as DMSO (use a bottle set aside for cell culture; open only in a laminar flow hood) or a freezing medium such as Synth-a-Freeze Cryopreservation Medium or Recovery Cell Culture Freezing Medium
33
+ • Disposable, sterile 15 mL or 50 mL conical tubes
34
+ • Reagents and equipment to determine viable and total cell counts (e.g., Countess Automated Cell Counter, or hemocytometer, cell counter, and Trypan Blue)
35
+ • Sterile cryogenic storage vials (i.e., cryovials)
36
+ • Controlled rate freezing apparatus or isopropanol chamber
37
+ • Liquid nitrogen storage container
38
+
39
+ For freezing adherent cells, in addition to the above materials, you will need:
40
+
41
+ • Balanced salt solution such as Gibco Dulbecco's Phosphate Buffered Saline (DPBS), containing no calcium, magnesium, or phenol red
42
+ • Dissociation reagent such as trypsin or Gibco TrypLE Express, without phenol red
43
+
44
+ Catalog numbers: J66650.AK, 339650, 5000-0020, 5100-0050, 14190250, 25200072, 12604039, 15250061, 12648010, R00550, A49862
45
+
46
+ MEDIUM FOR FREEZING CELLS
47
+ ================================================================================
48
+
49
+ Cryoprotective agents reduce the freezing point of the medium and slow the cooling rate, greatly reducing the risk of ice crystal formation, which can damage cells and cause cell death. Media for cryopreservation typically includes a base medium, a cryopreservative, and a protein source, but we recommend that you closely follow your cell line and product-specific instructions to determine the best cryopreservation media for your experiment. When included, the cryopreservative and protein protect the cells from the stress of the freeze-thaw process. The cells are then frozen in liquid nitrogen for preservation.
50
+
51
+ For media with serum, the constituents may be:
52
+
53
+ • complete medium containing 10% glycerol
54
+ • complete medium containing 10% DMSO (dimethyl sulfoxide)
55
+ • 50% cell-conditioned medium with 50% fresh medium with 10% glycerol or 10% DMSO
56
+
57
+ Safety Note: DMSO solutions are known to facilitate the entry process of organic molecules into tissues. You should always handle reagents containing DMSO using equipment and practices appropriate for the hazards posed by such materials and dispose of the reagents in compliance with local regulations.
58
+
59
+ A serum-free medium has generally low or no protein; but you can still use it as a base for a cryopreservative medium in the following formulations:
60
+
61
+ • 50% cell-conditioned serum-free medium with 50% fresh serum-free medium containing 7.5% DMSO
62
+ • fresh serum-free medium containing 7.5% DMSO and 10% cell culture–grade BSA
63
+
64
+ You may also use a specially formulated complete cryopreservation medium such as:
65
+
66
+ • Gibco Recovery Cell Culture Freezing is a ready-to-use complete cryopreservation medium for mammalian cell cultures with an optimized ratio of fetal bovine serum to bovine serum for improved cell viability and cell recovery after thawing.
67
+ • Gibco Synth-a-Freeze Cryopreservation Medium is a chemically defined, protein-free, sterile cryopreservation medium with 10% DMSO that is suitable for the cryopreservation of many stem and primary cell types apart from melanocytes.
68
+
69
+ PROTOCOL
70
+ ================================================================================
71
+
72
+ The proper way to freeze cells and cryopreserve your cell lines for future use requires diligent attention to passaging procedures and storage requirements. Remember to always wear personal protective equipment and use proper sterile technique when freezing cells.
73
+
74
+ The general freezing method is the same for adherent and suspension cells, except that adherent cells need to be removed from the culture plates before starting the freezing procedure. The following cell freezing protocol is a general procedure for cryopreserving cultured cells. Optimal freezing conditions depend on the cell line in use—for detailed protocols, always refer to the cell-specific product insert.
75
+
76
+ Prior to cryopreservation, cells should be characterized and checked for contamination. Freeze cells in log phase, at a high concentration of at least 90% viability, and at as low a passage number as possible, as this will lead to the best outcomes when your stock is thawed.
77
+
78
+ 1. Prepare freezing medium and store at 2° to 8°C until use.
79
+
80
+ 2. For adherent cells, gently detach cells from the tissue culture vessel following the procedure used during subculture. Detach as gently as possible to minimize damage to the cells.
81
+
82
+ 3. Resuspend the detached cells in the complete growth medium.
83
+
84
+ 4. Find the total number of cells and percent viability using a hemocytometer or the Countess Automated Cell Counter and Trypan Blue exclusion.
85
+
86
+ 5. According to the desired viable cell density, calculate the required volume of freezing medium.
87
+
88
+ 6. Centrifuge the cell suspension at approximately 100–400 × g for 5 to 10 minutes (centrifugation speed and duration varies depending on the cell type). Using a pipette, aseptically withdraw the supernatant to the smallest volume without disturbing the cell pellet and discard.
89
+
90
+ 7. Resuspend the cell pellet in cold freezing medium at the recommended viable cell density for the specific cell type.
91
+
92
+ 8. Dispense aliquots of the cell suspension into sterile cryogenic storage vials. As you aliquot them, gently and often mix the cells to maintain a homogeneous cell suspension.
93
+
94
+ 9. Freeze the cells slowly by reducing the temperature at approximately 1°C per minute using a controlled rate cryo-freezer or a cryo-freezing container such as "Mr. Frosty," available from Thermo Scientific Nalgene labware (Nalgene Nunc). Alternatively, place the cryovials containing the cells in an isopropanol chamber and store them at –80°C overnight.
95
+
96
+ 10. Transfer frozen cells to liquid nitrogen and store them in the gas phase above the liquid nitrogen below –135°C.
97
+
98
+ Safety Note: Biohazardous materials must be stored in the gas phase above the liquid nitrogen. Storing the sealed cryovials in the gas phase reduces the risk of explosion. If you are using liquid-phase storage, be aware of the explosion hazard with both glass and plastic cryovials and always wear a face shield or goggles.
99
+
100
+ REFERENCES
101
+ ================================================================================
102
+
103
+ Freshney, R. (1987) Culture of Animal Cells: A Manual of Basic Technique, p. 220, Alan R. Liss, Inc., New York.
104
+
105
+ Mohamed, H. M., Sundar, P., Ridwan, N. A. A., Cheong, A. J., Mohamad Salleh, N. A., Sulaiman, N., Mh Busra, F., & Maarof, M. (2024). Optimisation of cryopreservation conditions, including storage duration and revival methods, for the viability of human primary cells. BMC Molecular & Cell Biology, 25(1), 1–14. https://doi.org/10.1186/s12860-024-00516-6.
Biomni/biomni/tool/synthetic_biology.py ADDED
@@ -0,0 +1,1351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def engineer_bacterial_genome_for_therapeutic_delivery(bacterial_genome_file, genetic_parts):
2
+ """Engineer a bacterial genome by integrating therapeutic genetic parts for therapeutic delivery.
3
+
4
+ Parameters
5
+ ----------
6
+ bacterial_genome_file : str
7
+ Path to the file containing the bacterial genome sequence in FASTA format
8
+ genetic_parts : dict
9
+ Dictionary containing genetic parts to be integrated::
10
+
11
+ {
12
+ 'promoters': list of dict with 'name', 'sequence', and 'position',
13
+ 'genes': list of dict with 'name', 'sequence', and 'position',
14
+ 'terminators': list of dict with 'name', 'sequence', and 'position',
15
+ 'cargo': dict with 'name' and 'sequence' of the therapeutic cargo
16
+ }
17
+
18
+ Returns
19
+ -------
20
+ str
21
+ Research log summarizing the engineering process, including the filename of the
22
+ engineered genome and plasmid map
23
+
24
+ """
25
+ import datetime
26
+
27
+ from Bio import SeqIO
28
+ from Bio.Graphics import GenomeDiagram
29
+ from Bio.Seq import Seq
30
+ from Bio.SeqFeature import FeatureLocation, SeqFeature
31
+ from Bio.SeqRecord import SeqRecord
32
+ from reportlab.lib import colors
33
+
34
+ # Initialize research log
35
+ log = ["# Bacterial Genome Engineering for Therapeutic Delivery\n"]
36
+ log.append(f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
37
+
38
+ # Step 1: Load the bacterial genome
39
+ log.append("## Step 1: Loading Bacterial Genome")
40
+ try:
41
+ genome_record = SeqIO.read(bacterial_genome_file, "fasta")
42
+ genome_seq = genome_record.seq
43
+ log.append(f"Successfully loaded genome: {genome_record.id}")
44
+ log.append(f"Genome length: {len(genome_seq)} bp\n")
45
+ except Exception as e:
46
+ log.append(f"Error loading genome: {str(e)}")
47
+ return "\n".join(log)
48
+
49
+ # Step 2: Design and integrate genetic parts
50
+ log.append("## Step 2: Integrating Genetic Parts")
51
+
52
+ # Create a new genome sequence for modifications
53
+ # In newer BioPython versions, Seq objects are mutable by default
54
+ # or we can convert to a string and modify it, then convert back to Seq
55
+ engineered_seq = str(genome_seq)
56
+
57
+ # Track features for visualization
58
+ features = []
59
+
60
+ # Keep track of position adjustments as we add sequences
61
+ position_adjustment = 0
62
+
63
+ # Add promoters
64
+ if "promoters" in genetic_parts:
65
+ log.append("### Adding Promoters:")
66
+ for promoter in genetic_parts["promoters"]:
67
+ position = promoter["position"] + position_adjustment
68
+
69
+ # Insert the sequence at the position
70
+ engineered_seq = engineered_seq[:position] + promoter["sequence"] + engineered_seq[position:]
71
+ position_adjustment += len(promoter["sequence"])
72
+
73
+ # Adjust positions of subsequent elements
74
+ for part_type in ["genes", "terminators"]:
75
+ if part_type in genetic_parts:
76
+ for part in genetic_parts[part_type]:
77
+ if part["position"] > promoter["position"]:
78
+ part["position"] += len(promoter["sequence"])
79
+
80
+ # Add feature for visualization
81
+ feature = SeqFeature(
82
+ FeatureLocation(position, position + len(promoter["sequence"])),
83
+ type="promoter",
84
+ qualifiers={"label": promoter["name"]},
85
+ )
86
+ features.append(feature)
87
+
88
+ log.append(f" - Added promoter {promoter['name']} at position {position}")
89
+
90
+ # Add genes
91
+ if "genes" in genetic_parts:
92
+ log.append("### Adding Genes:")
93
+ for gene in genetic_parts["genes"]:
94
+ position = gene["position"] + position_adjustment
95
+
96
+ # Insert the sequence at the position
97
+ engineered_seq = engineered_seq[:position] + gene["sequence"] + engineered_seq[position:]
98
+ position_adjustment += len(gene["sequence"])
99
+
100
+ # Adjust positions of subsequent elements
101
+ for part_type in ["terminators"]:
102
+ if part_type in genetic_parts:
103
+ for part in genetic_parts[part_type]:
104
+ if part["position"] > gene["position"]:
105
+ part["position"] += len(gene["sequence"])
106
+
107
+ # Add feature for visualization
108
+ feature = SeqFeature(
109
+ FeatureLocation(position, position + len(gene["sequence"])),
110
+ type="gene",
111
+ qualifiers={"label": gene["name"]},
112
+ )
113
+ features.append(feature)
114
+
115
+ log.append(f" - Added gene {gene['name']} at position {position}")
116
+
117
+ # Add terminators
118
+ if "terminators" in genetic_parts:
119
+ log.append("### Adding Terminators:")
120
+ for terminator in genetic_parts["terminators"]:
121
+ position = terminator["position"] + position_adjustment
122
+
123
+ # Insert the sequence at the position
124
+ engineered_seq = engineered_seq[:position] + terminator["sequence"] + engineered_seq[position:]
125
+ position_adjustment += len(terminator["sequence"])
126
+
127
+ # Add feature for visualization
128
+ feature = SeqFeature(
129
+ FeatureLocation(position, position + len(terminator["sequence"])),
130
+ type="terminator",
131
+ qualifiers={"label": terminator["name"]},
132
+ )
133
+ features.append(feature)
134
+
135
+ log.append(f" - Added terminator {terminator['name']} at position {position}")
136
+
137
+ # Add therapeutic cargo
138
+ if "cargo" in genetic_parts:
139
+ cargo = genetic_parts["cargo"]
140
+ # Find a suitable position (after the last added element)
141
+ position = max([f.location.end for f in features]) if features else len(engineered_seq) // 2
142
+
143
+ # Insert the sequence at the position
144
+ engineered_seq = engineered_seq[:position] + cargo["sequence"] + engineered_seq[position:]
145
+
146
+ # Add feature for visualization
147
+ feature = SeqFeature(
148
+ FeatureLocation(position, position + len(cargo["sequence"])),
149
+ type="therapeutic_cargo",
150
+ qualifiers={"label": cargo["name"]},
151
+ )
152
+ features.append(feature)
153
+
154
+ log.append("### Added Therapeutic Cargo:")
155
+ log.append(f" - Added {cargo['name']} at position {position}")
156
+ log.append(f" - Cargo length: {len(cargo['sequence'])} bp\n")
157
+
158
+ # Step 3: Create the engineered genome record
159
+ log.append("## Step 3: Finalizing Engineered Genome")
160
+ engineered_genome = SeqRecord(
161
+ seq=Seq(engineered_seq),
162
+ id=f"{genome_record.id}_engineered",
163
+ name=f"{genome_record.id}_engineered",
164
+ description=f"Engineered {genome_record.id} for therapeutic delivery",
165
+ )
166
+
167
+ # Add all features to the engineered genome
168
+ engineered_genome.features = features
169
+
170
+ # Save the engineered genome to a file
171
+ output_genome_file = f"{genome_record.id}_engineered.fasta"
172
+ SeqIO.write(engineered_genome, output_genome_file, "fasta")
173
+ log.append(f"Engineered genome saved to: {output_genome_file}")
174
+ log.append(f"Total genome length: {len(engineered_genome.seq)} bp\n")
175
+
176
+ # Step 4: Generate a plasmid map
177
+ log.append("## Step 4: Generating Plasmid Map")
178
+
179
+ # Create the diagram
180
+ gd_diagram = GenomeDiagram.Diagram("Engineered Bacterial Genome")
181
+ gd_track = gd_diagram.new_track(1, name="Engineered Features")
182
+ gd_feature_set = gd_track.new_set()
183
+
184
+ # Add features with different colors
185
+ for feature in features:
186
+ if feature.type == "promoter":
187
+ color = colors.green
188
+ elif feature.type == "gene":
189
+ color = colors.blue
190
+ elif feature.type == "terminator":
191
+ color = colors.red
192
+ elif feature.type == "therapeutic_cargo":
193
+ color = colors.purple
194
+ else:
195
+ color = colors.grey
196
+
197
+ gd_feature_set.add_feature(feature, color=color, label=True)
198
+
199
+ # Draw and save the plasmid map
200
+ plasmid_map_file = f"{genome_record.id}_engineered_map.pdf"
201
+ gd_diagram.draw(format="circular", circular=True, pagesize=(1000, 1000))
202
+ gd_diagram.write(plasmid_map_file, "PDF")
203
+
204
+ log.append(f"Plasmid map generated and saved to: {plasmid_map_file}\n")
205
+
206
+ # Step 5: Final summary
207
+ log.append("## Summary of Engineered Bacterial Strain")
208
+ log.append(f"- Base strain: {genome_record.id}")
209
+ log.append("- Added genetic parts:")
210
+
211
+ if "promoters" in genetic_parts:
212
+ log.append(f" - Promoters: {len(genetic_parts['promoters'])}")
213
+ if "genes" in genetic_parts:
214
+ log.append(f" - Genes: {len(genetic_parts['genes'])}")
215
+ if "terminators" in genetic_parts:
216
+ log.append(f" - Terminators: {len(genetic_parts['terminators'])}")
217
+ if "cargo" in genetic_parts:
218
+ log.append(f" - Therapeutic cargo: {genetic_parts['cargo']['name']}")
219
+
220
+ log.append("\nThe engineered bacterial strain is ready for transformation and selection.")
221
+
222
+ return "\n".join(log)
223
+
224
+
225
+ def analyze_bacterial_growth_rate(time_points, od_measurements, strain_name="Unknown strain", output_dir="./"):
226
+ """Analyze bacterial growth data and extract growth parameters from OD600 measurements.
227
+
228
+ Parameters
229
+ ----------
230
+ time_points : list or numpy.ndarray
231
+ Time points at which OD600 measurements were taken (in hours)
232
+ od_measurements : list or numpy.ndarray
233
+ Optical density (OD600) measurements corresponding to each time point
234
+ strain_name : str, optional
235
+ Name of the bacterial strain being analyzed, default is "Unknown strain"
236
+ output_dir : str, optional
237
+ Directory where to save the output files, default is current directory
238
+
239
+ Returns
240
+ -------
241
+ str
242
+ A research log summarizing the analysis steps, fitted parameters, and results
243
+
244
+ """
245
+ import os
246
+ from datetime import datetime
247
+
248
+ import matplotlib
249
+
250
+ matplotlib.use("Agg") # Use non-interactive backend
251
+ import matplotlib.pyplot as plt
252
+ import numpy as np
253
+ from scipy.optimize import curve_fit
254
+
255
+ # Ensure output directory exists
256
+ os.makedirs(output_dir, exist_ok=True)
257
+
258
+ # Convert inputs to numpy arrays if they aren't already
259
+ time_points = np.array(time_points)
260
+ od_measurements = np.array(od_measurements)
261
+
262
+ # Define the Gompertz growth model function
263
+ def gompertz_model(t, lag, mu_max, A):
264
+ """Gompertz growth model.
265
+
266
+ Parameters
267
+ ----------
268
+ t: time points
269
+ lag: lag time (hours)
270
+ mu_max: maximum growth rate (per hour)
271
+ A: carrying capacity (maximum OD)
272
+
273
+ """
274
+ return A * np.exp(-np.exp(mu_max * np.exp(1) * (lag - t) / A + 1))
275
+
276
+ # Initial parameter guesses
277
+ p0 = [
278
+ np.mean(time_points) / 3, # lag time guess: 1/3 of the mean time
279
+ 0.5, # growth rate guess: 0.5 per hour
280
+ max(od_measurements) * 1.1, # carrying capacity: slightly above max OD
281
+ ]
282
+
283
+ # Fit the model to the data
284
+ try:
285
+ popt, pcov = curve_fit(
286
+ gompertz_model,
287
+ time_points,
288
+ od_measurements,
289
+ p0=p0,
290
+ bounds=([0, 0, 0], [np.inf, np.inf, np.inf]),
291
+ )
292
+ lag_time, mu_max, carrying_capacity = popt
293
+
294
+ # Calculate doubling time (ln(2)/μ)
295
+ doubling_time = np.log(2) / mu_max
296
+
297
+ # Generate fitted curve for plotting
298
+ t_smooth = np.linspace(min(time_points), max(time_points), 100)
299
+ od_fit = gompertz_model(t_smooth, *popt)
300
+
301
+ # Create growth curve plot
302
+ plt.figure(figsize=(10, 6))
303
+ plt.scatter(time_points, od_measurements, label="Observed OD600")
304
+ plt.plot(t_smooth, od_fit, "r-", label="Fitted Gompertz model")
305
+ plt.xlabel("Time (hours)")
306
+ plt.ylabel("Optical Density (OD600)")
307
+ plt.title(f"Bacterial Growth Curve: {strain_name}")
308
+ plt.legend()
309
+ plt.grid(True, linestyle="--", alpha=0.7)
310
+
311
+ # Save the plot
312
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
313
+ plot_filename = os.path.join(output_dir, f"{strain_name.replace(' ', '_')}_{timestamp}_growth_curve.png")
314
+ plt.savefig(plot_filename)
315
+ plt.close()
316
+
317
+ # Create research log
318
+ log = f"""## Bacterial Growth Rate Analysis: {strain_name}
319
+
320
+ ### Analysis Summary
321
+ - **Date and Time:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
322
+ - **Sample:** {strain_name}
323
+ - **Data Points:** {len(time_points)} measurements over {max(time_points) - min(time_points):.2f} hours
324
+
325
+ ### Growth Parameters (Gompertz Model)
326
+ - **Lag Phase Duration:** {lag_time:.2f} hours
327
+ - **Maximum Growth Rate (μ_max):** {mu_max:.4f} per hour
328
+ - **Doubling Time:** {doubling_time:.2f} hours
329
+ - **Carrying Capacity (max OD):** {carrying_capacity:.4f} OD600 units
330
+
331
+ ### Methods
332
+ 1. OD600 measurements were taken at various time points
333
+ 2. Data was fitted to the Gompertz growth model
334
+ 3. Growth parameters were extracted from the fitted model
335
+ 4. Growth curve was plotted and saved as: {plot_filename}
336
+
337
+ ### Note
338
+ The Gompertz model provides a good representation of bacterial growth phases including lag, exponential, and stationary phases.
339
+ """
340
+
341
+ return log
342
+
343
+ except RuntimeError as e:
344
+ return f"""## Bacterial Growth Rate Analysis: {strain_name}
345
+
346
+ ### Error in Analysis
347
+ Failed to fit the growth model to the provided data: {str(e)}
348
+
349
+ ### Suggestions
350
+ - Check if the data shows a typical growth pattern
351
+ - Ensure sufficient data points are provided
352
+ - Try different initial parameter guesses or a different growth model
353
+ """
354
+
355
+
356
+ def analyze_barcode_sequencing_data(
357
+ input_file,
358
+ barcode_pattern=None,
359
+ flanking_seq_5prime=None,
360
+ flanking_seq_3prime=None,
361
+ min_count=5,
362
+ output_dir="./results",
363
+ ):
364
+ """Analyze sequencing data to extract, quantify and determine lineage relationships of barcodes.
365
+
366
+ Parameters
367
+ ----------
368
+ input_file : str
369
+ Path to the input sequencing file in FASTQ or FASTA format
370
+ barcode_pattern : str, optional
371
+ Regular expression pattern to identify barcodes. If None, will use flanking sequences
372
+ flanking_seq_5prime : str, optional
373
+ 5' flanking sequence of the barcode region
374
+ flanking_seq_3prime : str, optional
375
+ 3' flanking sequence of the barcode region
376
+ min_count : int, default=5
377
+ Minimum count threshold for considering a barcode
378
+ output_dir : str, default="./results"
379
+ Directory to save output files
380
+
381
+ Returns
382
+ -------
383
+ str
384
+ Research log summarizing the analysis steps and results
385
+
386
+ """
387
+ import os
388
+ import re
389
+ from collections import Counter
390
+
391
+ import numpy as np
392
+ from Bio import SeqIO
393
+ from scipy.cluster.hierarchy import fcluster, linkage
394
+ from scipy.spatial.distance import pdist
395
+
396
+ # Create output directory if it doesn't exist
397
+ os.makedirs(output_dir, exist_ok=True)
398
+
399
+ # Initialize research log
400
+ log = "# Barcode Sequencing Analysis Research Log\n\n"
401
+ log += f"## Input Data\n- File: {input_file}\n\n"
402
+
403
+ # Step 1: Read sequences and extract barcodes
404
+ log += "## Step 1: Extracting barcodes from sequencing data\n"
405
+
406
+ # Determine file format based on extension
407
+ file_format = "fastq" if input_file.endswith((".fastq", ".fq")) else "fasta"
408
+
409
+ barcodes = []
410
+ total_reads = 0
411
+
412
+ for record in SeqIO.parse(input_file, file_format):
413
+ total_reads += 1
414
+ seq_str = str(record.seq)
415
+
416
+ # Extract barcode using pattern or flanking sequences
417
+ if barcode_pattern:
418
+ match = re.search(barcode_pattern, seq_str)
419
+ if match:
420
+ barcodes.append(match.group(0))
421
+ elif flanking_seq_5prime and flanking_seq_3prime:
422
+ pattern = f"{flanking_seq_5prime}(.*?){flanking_seq_3prime}"
423
+ match = re.search(pattern, seq_str)
424
+ if match:
425
+ barcodes.append(match.group(1))
426
+
427
+ log += f"- Total reads processed: {total_reads}\n"
428
+ log += f"- Barcodes extracted: {len(barcodes)}\n\n"
429
+
430
+ if len(barcodes) == 0:
431
+ log += "ERROR: No barcodes found. Check your barcode pattern or flanking sequences.\n"
432
+ return log
433
+
434
+ # Step 2: Quantify barcode abundances
435
+ log += "## Step 2: Quantifying barcode abundances\n"
436
+ barcode_counts = Counter(barcodes)
437
+
438
+ # Filter low-abundance barcodes
439
+ filtered_barcodes = {bc: count for bc, count in barcode_counts.items() if count >= min_count}
440
+
441
+ log += f"- Unique barcodes: {len(barcode_counts)}\n"
442
+ log += f"- Barcodes with count ≥ {min_count}: {len(filtered_barcodes)}\n"
443
+
444
+ # Save barcode counts to file
445
+ count_file = os.path.join(output_dir, "barcode_counts.tsv")
446
+ with open(count_file, "w") as f:
447
+ f.write("Barcode\tCount\tFrequency\n")
448
+ for bc, count in sorted(filtered_barcodes.items(), key=lambda x: x[1], reverse=True):
449
+ freq = count / total_reads
450
+ f.write(f"{bc}\t{count}\t{freq:.6f}\n")
451
+
452
+ log += f"- Barcode abundances saved to: {count_file}\n\n"
453
+
454
+ # Step 3: Analyze barcode relationships (lineage analysis)
455
+ log += "## Step 3: Analyzing barcode relationships and lineages\n"
456
+
457
+ # Only proceed if we have enough barcodes
458
+ if len(filtered_barcodes) < 2:
459
+ log += "- Not enough barcodes for lineage analysis after filtering\n\n"
460
+ else:
461
+ # Convert barcodes to numerical representation for distance calculation
462
+ barcode_list = list(filtered_barcodes.keys())
463
+
464
+ # Create a simple distance matrix based on Hamming distance
465
+ def hamming_distance(s1, s2):
466
+ # If sequences have different lengths, pad the shorter one
467
+ if len(s1) != len(s2):
468
+ max_len = max(len(s1), len(s2))
469
+ s1 = s1.ljust(max_len)
470
+ s2 = s2.ljust(max_len)
471
+ return sum(c1 != c2 for c1, c2 in zip(s1, s2, strict=False))
472
+
473
+ # Create distance matrix
474
+ n = len(barcode_list)
475
+ dist_matrix = np.zeros((n, n))
476
+ for i in range(n):
477
+ for j in range(i + 1, n):
478
+ dist = hamming_distance(barcode_list[i], barcode_list[j])
479
+ dist_matrix[i, j] = dist
480
+ dist_matrix[j, i] = dist
481
+
482
+ # Perform hierarchical clustering
483
+ try:
484
+ condensed_dist = pdist(dist_matrix)
485
+ Z = linkage(condensed_dist, method="average")
486
+ max_dist = 3 # Max Hamming distance to consider barcodes in same lineage
487
+ clusters = fcluster(Z, max_dist, criterion="distance")
488
+
489
+ # Count clusters
490
+ cluster_counts = Counter(clusters)
491
+
492
+ log += f"- Identified {len(cluster_counts)} potential lineages\n"
493
+ log += f"- Largest lineage contains {max(cluster_counts.values())} barcodes\n"
494
+
495
+ # Save lineage information
496
+ lineage_file = os.path.join(output_dir, "barcode_lineages.tsv")
497
+ with open(lineage_file, "w") as f:
498
+ f.write("Barcode\tCount\tLineage\n")
499
+ for i, bc in enumerate(barcode_list):
500
+ f.write(f"{bc}\t{filtered_barcodes[bc]}\t{clusters[i]}\n")
501
+
502
+ log += f"- Lineage assignments saved to: {lineage_file}\n\n"
503
+ except Exception as e:
504
+ log += f"- Error in lineage analysis: {str(e)}\n\n"
505
+
506
+ # Step 4: Summary
507
+ log += "## Summary\n"
508
+ log += f"- Processed {total_reads} sequencing reads\n"
509
+ log += f"- Identified {len(barcode_counts)} unique barcodes\n"
510
+ log += f"- {len(filtered_barcodes)} barcodes passed abundance threshold (≥ {min_count})\n"
511
+
512
+ return log
513
+
514
+
515
+ def analyze_bifurcation_diagram(time_series_data, parameter_values, system_name="Dynamical System", output_dir="./"):
516
+ """Performs bifurcation analysis on a dynamical system and generates a bifurcation diagram.
517
+
518
+ Parameters
519
+ ----------
520
+ time_series_data : numpy.ndarray
521
+ A 2D array where each row represents a time series for a specific parameter value.
522
+ Shape should be (n_parameter_values, n_time_points).
523
+ parameter_values : numpy.ndarray
524
+ 1D array of parameter values corresponding to each time series.
525
+ Shape should be (n_parameter_values,).
526
+ system_name : str, optional
527
+ Name of the dynamical system being analyzed, used for plot titles.
528
+ output_dir : str, optional
529
+ Directory to save the output files.
530
+
531
+ Returns
532
+ -------
533
+ str
534
+ Research log summarizing the bifurcation analysis process and results.
535
+
536
+ """
537
+ import os
538
+
539
+ import matplotlib
540
+
541
+ matplotlib.use("Agg") # Use non-interactive backend
542
+ import matplotlib.pyplot as plt
543
+ import numpy as np
544
+ from scipy.signal import find_peaks
545
+
546
+ # Create output directory if it doesn't exist
547
+ if not os.path.exists(output_dir):
548
+ os.makedirs(output_dir)
549
+
550
+ # Initialize research log
551
+ research_log = f"# Bifurcation Analysis for {system_name}\n\n"
552
+ research_log += "## Analysis Steps\n\n"
553
+
554
+ # Step 1: Verify input data
555
+ research_log += "### Step 1: Data Verification\n"
556
+ if len(parameter_values) != time_series_data.shape[0]:
557
+ research_log += "Error: Number of parameter values does not match number of time series.\n"
558
+ return research_log
559
+
560
+ research_log += f"- Analyzed {len(parameter_values)} parameter values\n"
561
+ research_log += f"- Each time series contains {time_series_data.shape[1]} data points\n\n"
562
+
563
+ # Step 2: Calculate indicators for different dynamical regimes
564
+ research_log += "### Step 2: Calculating Dynamical Indicators\n"
565
+
566
+ # Initialize arrays to store results
567
+ local_maxima_counts = np.zeros(len(parameter_values))
568
+ lyapunov_estimates = np.zeros(len(parameter_values))
569
+ attractor_points = []
570
+
571
+ for i, series in enumerate(time_series_data):
572
+ # Use the last 70% of the time series to avoid transients
573
+ steady_state = series[int(0.7 * len(series)) :]
574
+
575
+ # Find local maxima (peaks) to identify periodicity
576
+ peaks, _ = find_peaks(steady_state)
577
+ local_maxima_counts[i] = len(peaks)
578
+
579
+ # Simple estimate of the largest Lyapunov exponent
580
+ # Positive values suggest chaos, negative values suggest stability
581
+ if len(steady_state) > 10:
582
+ diffs = np.abs(np.diff(steady_state))
583
+ if np.mean(diffs) > 0:
584
+ lyapunov_estimates[i] = np.log(np.mean(diffs))
585
+ else:
586
+ lyapunov_estimates[i] = -1.0 # Stable behavior
587
+
588
+ # Collect points for the bifurcation diagram
589
+ # We use local maxima as sampling points for the attractor
590
+ if len(peaks) > 0:
591
+ attractor_points.append((parameter_values[i], steady_state[peaks]))
592
+ else:
593
+ # If no peaks, use the last few points
594
+ attractor_points.append((parameter_values[i], steady_state[-5:]))
595
+
596
+ research_log += "- Calculated approximate Lyapunov exponents\n"
597
+ research_log += "- Identified local maxima for periodicity analysis\n"
598
+ research_log += "- Sampled attractor points for bifurcation diagram\n\n"
599
+
600
+ # Step 3: Classify dynamical regimes
601
+ research_log += "### Step 3: Classifying Dynamical Regimes\n"
602
+
603
+ # Initialize arrays for regime classification
604
+ regimes = np.full(len(parameter_values), "unknown", dtype=object)
605
+
606
+ # Classify based on Lyapunov exponent and periodicity
607
+ for i in range(len(parameter_values)):
608
+ if lyapunov_estimates[i] > 0.05:
609
+ regimes[i] = "chaotic"
610
+ elif local_maxima_counts[i] == 0:
611
+ regimes[i] = "stable"
612
+ elif local_maxima_counts[i] == 1:
613
+ regimes[i] = "period-1"
614
+ elif local_maxima_counts[i] == 2:
615
+ regimes[i] = "period-2"
616
+ elif local_maxima_counts[i] == 4:
617
+ regimes[i] = "period-4"
618
+ elif local_maxima_counts[i] > 4:
619
+ if lyapunov_estimates[i] > 0:
620
+ regimes[i] = "chaotic"
621
+ else:
622
+ regimes[i] = f"period-{int(local_maxima_counts[i])}"
623
+
624
+ # Count regime types
625
+ unique_regimes = np.unique(regimes)
626
+ for regime in unique_regimes:
627
+ count = np.sum(regimes == regime)
628
+ research_log += f"- Identified {count} parameter values in '{regime}' regime\n"
629
+
630
+ research_log += "\n"
631
+
632
+ # Step 4: Create bifurcation diagram
633
+ research_log += "### Step 4: Creating Bifurcation Diagram\n"
634
+
635
+ plt.figure(figsize=(12, 8))
636
+
637
+ # Plot bifurcation points
638
+ for param_val, points in attractor_points:
639
+ y_values = points.flatten() # Flatten in case points is multi-dimensional
640
+ plt.plot([param_val] * len(y_values), y_values, "k.", markersize=0.5)
641
+
642
+ # Highlight different regimes with colored background
643
+ regime_changes = np.where(regimes[:-1] != regimes[1:])[0]
644
+ regime_boundaries = (
645
+ [parameter_values[0]] + [parameter_values[i + 1] for i in regime_changes] + [parameter_values[-1]]
646
+ )
647
+
648
+ colors = {
649
+ "stable": "lightblue",
650
+ "period-1": "lightgreen",
651
+ "period-2": "lightyellow",
652
+ "period-4": "lightpink",
653
+ "chaotic": "salmon",
654
+ }
655
+
656
+ for i in range(len(regime_boundaries) - 1):
657
+ start_idx = list(parameter_values).index(regime_boundaries[i])
658
+ regime = regimes[start_idx]
659
+ if regime in colors:
660
+ plt.axvspan(
661
+ regime_boundaries[i],
662
+ regime_boundaries[i + 1],
663
+ alpha=0.3,
664
+ color=colors.get(regime, "lightgray"),
665
+ )
666
+
667
+ plt.xlabel("Parameter Value")
668
+ plt.ylabel("State Variable")
669
+ plt.title(f"Bifurcation Diagram for {system_name}")
670
+
671
+ # Add legend for regimes
672
+ from matplotlib.patches import Patch
673
+
674
+ legend_elements = [
675
+ Patch(facecolor=colors.get(regime, "lightgray"), alpha=0.3, label=regime)
676
+ for regime in unique_regimes
677
+ if regime in colors
678
+ ]
679
+ plt.legend(handles=legend_elements, loc="upper right")
680
+
681
+ # Save the figure
682
+ output_file = os.path.join(output_dir, f"{system_name.replace(' ', '_')}_bifurcation_diagram.png")
683
+ plt.savefig(output_file, dpi=300)
684
+ plt.close()
685
+
686
+ research_log += "- Created bifurcation diagram showing different dynamical regimes\n"
687
+ research_log += f"- Diagram saved as: {output_file}\n\n"
688
+
689
+ # Step 5: Identify regime transitions
690
+ research_log += "### Step 5: Identifying Regime Transitions\n"
691
+
692
+ if len(regime_changes) > 0:
693
+ for i in regime_changes:
694
+ research_log += f"- Transition from '{regimes[i]}' to '{regimes[i + 1]}' at parameter value ≈ {parameter_values[i + 1]:.4f}\n"
695
+ else:
696
+ research_log += "- No regime transitions detected in the parameter range\n"
697
+
698
+ research_log += "\n### Summary\n"
699
+ research_log += f"Bifurcation analysis completed for {system_name}. "
700
+ research_log += f"The system exhibits {len(unique_regimes)} different dynamical regimes "
701
+ research_log += f"across the parameter range [{parameter_values[0]:.4f}, {parameter_values[-1]:.4f}].\n"
702
+
703
+ return research_log
704
+
705
+
706
+ def create_biochemical_network_sbml_model(reaction_network, kinetic_parameters, output_file="biochemical_model.xml"):
707
+ """Generate a mathematical model of a biochemical network in SBML format.
708
+
709
+ Parameters
710
+ ----------
711
+ reaction_network : list of dict
712
+ List of dictionaries, each representing a reaction with keys:
713
+ - 'id': Reaction identifier
714
+ - 'name': Reaction name
715
+ - 'reactants': Dict of reactant species IDs and their stoichiometry
716
+ - 'products': Dict of product species IDs and their stoichiometry
717
+ - 'reversible': Boolean indicating if reaction is reversible
718
+
719
+ kinetic_parameters : dict
720
+ Dictionary mapping reaction IDs to their kinetic law parameters.
721
+ Each entry should contain:
722
+ - 'law_type': Type of kinetic law (e.g., 'mass_action', 'michaelis_menten')
723
+ - 'parameters': Dict of parameter names and values
724
+
725
+ output_file : str, optional
726
+ File path to save the SBML model (default: "biochemical_model.xml")
727
+
728
+ Returns
729
+ -------
730
+ str
731
+ Research log summarizing the model creation process
732
+
733
+ """
734
+ import os
735
+
736
+ import libsbml
737
+
738
+ log = "# Biochemical Network SBML Model Creation Log\n\n"
739
+
740
+ # Step 1: Create an SBML document
741
+ log += "## Step 1: Creating SBML document\n"
742
+ sbml_ns = libsbml.SBMLNamespaces(3, 2) # SBML Level 3 Version 2
743
+ document = libsbml.SBMLDocument(sbml_ns)
744
+ model = document.createModel()
745
+ model.setId("biochemical_network_model")
746
+ model.setName("Biochemical Network Model")
747
+ log += "- Created SBML Level 3 Version 2 document\n"
748
+ log += "- Created model with ID 'biochemical_network_model'\n\n"
749
+
750
+ # Step 2: Create default compartment
751
+ log += "## Step 2: Creating default compartment\n"
752
+ compartment = model.createCompartment()
753
+ compartment.setId("default")
754
+ compartment.setConstant(True)
755
+ compartment.setSize(1.0)
756
+ compartment.setSpatialDimensions(3)
757
+ log += "- Created default compartment\n\n"
758
+
759
+ # Step 3: Create species
760
+ log += "## Step 3: Creating species\n"
761
+ species_set = set()
762
+
763
+ # Collect all unique species from reactions
764
+ for reaction in reaction_network:
765
+ for species_id in list(reaction["reactants"].keys()) + list(reaction["products"].keys()):
766
+ species_set.add(species_id)
767
+
768
+ # Create species in the model
769
+ for species_id in species_set:
770
+ species = model.createSpecies()
771
+ species.setId(species_id)
772
+ species.setCompartment("default")
773
+ species.setInitialConcentration(0.0) # Default initial concentration
774
+ species.setHasOnlySubstanceUnits(False)
775
+ species.setBoundaryCondition(False)
776
+ species.setConstant(False)
777
+ log += f"- Created species '{species_id}'\n"
778
+ log += "\n"
779
+
780
+ # Step 4: Create reactions
781
+ log += "## Step 4: Creating reactions and kinetic laws\n"
782
+ for reaction_data in reaction_network:
783
+ reaction = model.createReaction()
784
+ reaction.setId(reaction_data["id"])
785
+ reaction.setName(reaction_data["name"])
786
+ reaction.setReversible(reaction_data["reversible"])
787
+ reaction.setFast(False)
788
+
789
+ # Add reactants
790
+ for reactant_id, stoichiometry in reaction_data["reactants"].items():
791
+ species_ref = reaction.createReactant()
792
+ species_ref.setSpecies(reactant_id)
793
+ species_ref.setStoichiometry(stoichiometry)
794
+ species_ref.setConstant(True)
795
+
796
+ # Add products
797
+ for product_id, stoichiometry in reaction_data["products"].items():
798
+ species_ref = reaction.createProduct()
799
+ species_ref.setSpecies(product_id)
800
+ species_ref.setStoichiometry(stoichiometry)
801
+ species_ref.setConstant(True)
802
+
803
+ # Create kinetic law
804
+ if reaction_data["id"] in kinetic_parameters:
805
+ kinetic_data = kinetic_parameters[reaction_data["id"]]
806
+ kinetic_law = reaction.createKineticLaw()
807
+
808
+ # Add parameters to kinetic law
809
+ for param_name, param_value in kinetic_data["parameters"].items():
810
+ parameter = kinetic_law.createParameter()
811
+ parameter.setId(param_name)
812
+ parameter.setValue(param_value)
813
+
814
+ # Set the appropriate formula based on the law type
815
+ if kinetic_data["law_type"] == "mass_action":
816
+ # Simple mass action kinetics
817
+ reactants_formula = " * ".join([f"{reactant_id}" for reactant_id in reaction_data["reactants"]])
818
+ formula = f"k * {reactants_formula}" if reactants_formula else "k"
819
+ kinetic_law.setMath(libsbml.parseL3Formula(formula))
820
+ elif kinetic_data["law_type"] == "michaelis_menten":
821
+ # Michaelis-Menten kinetics (assuming single substrate)
822
+ substrate = list(reaction_data["reactants"].keys())[0] if reaction_data["reactants"] else "S"
823
+ formula = f"Vmax * {substrate} / (Km + {substrate})"
824
+ kinetic_law.setMath(libsbml.parseL3Formula(formula))
825
+ # Custom formula provided in the parameters
826
+ elif "formula" in kinetic_data:
827
+ kinetic_law.setMath(libsbml.parseL3Formula(kinetic_data["formula"]))
828
+
829
+ log += f"- Created reaction '{reaction_data['id']}' ({reaction_data['name']})\n"
830
+ if reaction_data["id"] in kinetic_parameters:
831
+ log += f" - Added kinetic law of type '{kinetic_parameters[reaction_data['id']]['law_type']}'\n"
832
+ log += "\n"
833
+
834
+ # Step 5: Validate the model
835
+ log += "## Step 5: Validating the SBML model\n"
836
+ document.setConsistencyChecks(libsbml.LIBSBML_CAT_UNITS_CONSISTENCY, False)
837
+ consistent = document.checkConsistency()
838
+ if consistent == 0:
839
+ log += "- Model passed consistency check\n"
840
+ else:
841
+ log += "- Model has consistency issues:\n"
842
+ for i in range(document.getNumErrors()):
843
+ error = document.getError(i)
844
+ log += f" - {error.getMessage()}\n"
845
+ log += "\n"
846
+
847
+ # Step 6: Write model to file
848
+ log += "## Step 6: Saving the SBML model\n"
849
+ libsbml.writeSBMLToFile(document, output_file)
850
+ log += f"- Model saved to '{os.path.abspath(output_file)}'\n\n"
851
+
852
+ # Step 7: Summary
853
+ log += "## Summary\n"
854
+ log += f"- Created SBML model with {len(species_set)} species and {len(reaction_network)} reactions\n"
855
+ log += f"- Model saved as '{output_file}'\n"
856
+
857
+ return log
858
+
859
+
860
+ def optimize_codons_for_heterologous_expression(target_sequence, host_codon_usage):
861
+ """Analyzes and optimizes a DNA/RNA sequence for improved expression in a heterologous host organism.
862
+
863
+ Parameters
864
+ ----------
865
+ target_sequence : str
866
+ The DNA or RNA sequence of the target gene to be optimized.
867
+ Should contain complete codons (length divisible by 3).
868
+
869
+ host_codon_usage : dict
870
+ Dictionary mapping codons to their usage frequency in the host organism.
871
+ Format: {'AUG': 0.8, 'GCC': 0.6, ...} or {'ATG': 0.8, 'GCC': 0.6, ...}
872
+
873
+ Returns
874
+ -------
875
+ str
876
+ A research log summarizing the optimization process and results.
877
+
878
+ """
879
+ from Bio.Data import CodonTable
880
+
881
+ # Initialize research log
882
+ log = "# Codon Optimization Analysis Research Log\n\n"
883
+
884
+ # Check if sequence is DNA or RNA and standardize to DNA
885
+ is_rna = "U" in target_sequence
886
+ working_seq = target_sequence.replace("U", "T") if is_rna else target_sequence
887
+
888
+ log += "## Input Sequence Analysis\n"
889
+ log += f"- Sequence type: {'RNA' if is_rna else 'DNA'}\n"
890
+ log += f"- Sequence length: {len(working_seq)} nucleotides\n"
891
+
892
+ # Verify sequence length is divisible by 3
893
+ if len(working_seq) % 3 != 0:
894
+ log += (
895
+ f"- WARNING: Sequence length ({len(working_seq)}) is not divisible by 3. Optimization may be incomplete.\n"
896
+ )
897
+
898
+ # Extract codons from the sequence
899
+ original_codons = [working_seq[i : i + 3] for i in range(0, len(working_seq), 3)]
900
+ log += f"- Number of codons: {len(original_codons)}\n\n"
901
+
902
+ # Get standard genetic code
903
+ standard_table = CodonTable.standard_dna_table
904
+
905
+ # Create codon to amino acid mapping
906
+ codon_to_aa = {}
907
+ for codon, amino_acid in standard_table.forward_table.items():
908
+ codon_to_aa[codon] = amino_acid
909
+
910
+ # Add stop codons
911
+ for stop_codon in standard_table.stop_codons:
912
+ codon_to_aa[stop_codon] = "*" # * represents stop codon
913
+
914
+ # Create amino acid to codon mapping with host frequencies
915
+ aa_to_codons = {}
916
+ for codon, aa in codon_to_aa.items():
917
+ if aa not in aa_to_codons:
918
+ aa_to_codons[aa] = []
919
+ # Use host frequency or default to 0 if not provided
920
+ frequency = host_codon_usage.get(codon, 0)
921
+ aa_to_codons[aa].append((codon, frequency))
922
+
923
+ # Sort codons by frequency for each amino acid
924
+ for aa in aa_to_codons:
925
+ aa_to_codons[aa] = sorted(aa_to_codons[aa], key=lambda x: x[1], reverse=True)
926
+
927
+ # Optimize codons
928
+ optimized_codons = []
929
+ for codon in original_codons:
930
+ try:
931
+ aa = codon_to_aa[codon]
932
+ # Select the highest frequency codon for this amino acid
933
+ best_codon = aa_to_codons[aa][0][0] if aa_to_codons[aa] else codon
934
+ optimized_codons.append(best_codon)
935
+ except KeyError:
936
+ # If codon not in standard table, keep original
937
+ optimized_codons.append(codon)
938
+ log += f"- WARNING: Non-standard codon detected: {codon}. Keeping original.\n"
939
+
940
+ # Combine optimized codons into sequence
941
+ optimized_sequence = "".join(optimized_codons)
942
+
943
+ # Convert back to RNA if input was RNA
944
+ if is_rna:
945
+ optimized_sequence = optimized_sequence.replace("T", "U")
946
+
947
+ # Calculate optimization statistics
948
+ codon_changes = sum(1 for i, j in zip(original_codons, optimized_codons, strict=False) if i != j)
949
+ percent_changed = (codon_changes / len(original_codons)) * 100 if original_codons else 0
950
+
951
+ log += "## Optimization Results\n"
952
+ log += f"- Codons modified: {codon_changes} out of {len(original_codons)} ({percent_changed:.2f}%)\n"
953
+
954
+ # Save sequences to files
955
+ with open("original_sequence.txt", "w") as f:
956
+ f.write(target_sequence)
957
+
958
+ with open("optimized_sequence.txt", "w") as f:
959
+ f.write(optimized_sequence)
960
+
961
+ log += "- Original sequence saved to: original_sequence.txt\n"
962
+ log += "- Optimized sequence saved to: optimized_sequence.txt\n\n"
963
+
964
+ # Add summary
965
+ log += "## Summary\n"
966
+ log += "The target gene sequence has been optimized for expression in the provided host organism. "
967
+ log += f"The optimization process replaced {codon_changes} codons ({percent_changed:.2f}%) "
968
+ log += "with synonymous codons that have higher usage frequency in the host.\n"
969
+
970
+ return log
971
+
972
+
973
+ def simulate_gene_circuit_with_growth_feedback(
974
+ circuit_topology,
975
+ kinetic_params,
976
+ growth_params,
977
+ simulation_time=100,
978
+ time_points=1000,
979
+ ):
980
+ """Simulate gene regulatory circuit dynamics with growth feedback.
981
+
982
+ Parameters
983
+ ----------
984
+ circuit_topology : numpy.ndarray
985
+ Adjacency matrix representing the gene circuit topology.
986
+ Positive values indicate activation, negative values indicate repression.
987
+ Shape should be (n_genes, n_genes) where n_genes is the number of genes in the circuit.
988
+
989
+ kinetic_params : dict
990
+ Dictionary containing kinetic parameters:
991
+ - 'basal_rates': list of basal expression rates for each gene
992
+ - 'degradation_rates': list of degradation rates for each gene
993
+ - 'hill_coefficients': list of Hill coefficients for regulatory interactions
994
+ - 'threshold_constants': list of threshold constants for regulatory interactions
995
+
996
+ growth_params : dict
997
+ Dictionary containing growth-related parameters:
998
+ - 'max_growth_rate': maximum cell growth rate
999
+ - 'growth_inhibition': how gene expression affects growth
1000
+ - 'gene_growth_weights': weights for how each gene affects growth
1001
+
1002
+ simulation_time : float, optional
1003
+ Total simulation time (default: 100)
1004
+
1005
+ time_points : int, optional
1006
+ Number of time points to sample (default: 1000)
1007
+
1008
+ Returns
1009
+ -------
1010
+ str
1011
+ Research log summarizing the simulation and results with file names of saved data
1012
+
1013
+ """
1014
+ import datetime
1015
+ import json
1016
+ import os
1017
+
1018
+ import numpy as np
1019
+ from scipy.integrate import solve_ivp
1020
+
1021
+ # Extract parameters
1022
+ n_genes = circuit_topology.shape[0]
1023
+ basal_rates = kinetic_params["basal_rates"]
1024
+ degradation_rates = kinetic_params["degradation_rates"]
1025
+ hill_coefficients = kinetic_params["hill_coefficients"]
1026
+ threshold_constants = kinetic_params["threshold_constants"]
1027
+
1028
+ max_growth_rate = growth_params["max_growth_rate"]
1029
+ growth_inhibition = growth_params["growth_inhibition"]
1030
+ gene_growth_weights = growth_params["gene_growth_weights"]
1031
+
1032
+ # Define the ODE system
1033
+ def gene_circuit_odes(t, state):
1034
+ # Extract state variables
1035
+ gene_expressions = state[:n_genes]
1036
+ cell_mass = state[n_genes]
1037
+
1038
+ # Initialize derivatives
1039
+ dxdt = np.zeros(n_genes + 1)
1040
+
1041
+ # Gene expression dynamics
1042
+ for i in range(n_genes):
1043
+ # Basal expression
1044
+ production = basal_rates[i]
1045
+
1046
+ # Regulatory influences
1047
+ for j in range(n_genes):
1048
+ if circuit_topology[i, j] != 0:
1049
+ # Calculate regulatory effect
1050
+ regulation = gene_expressions[j] ** hill_coefficients[j] / (
1051
+ threshold_constants[j] ** hill_coefficients[j] + gene_expressions[j] ** hill_coefficients[j]
1052
+ )
1053
+
1054
+ if circuit_topology[i, j] > 0: # Activation
1055
+ production *= 1 + circuit_topology[i, j] * regulation
1056
+ else: # Repression
1057
+ production *= 1 + circuit_topology[i, j] * (1 - regulation)
1058
+
1059
+ # Dilution due to growth and degradation
1060
+ dilution = degradation_rates[i] + (dxdt[n_genes] / cell_mass if cell_mass > 0 else 0)
1061
+
1062
+ # Final rate equation for gene i
1063
+ dxdt[i] = production - dilution * gene_expressions[i]
1064
+
1065
+ # Cell growth dynamics
1066
+ growth_burden = sum(gene_growth_weights[i] * gene_expressions[i] for i in range(n_genes))
1067
+ dxdt[n_genes] = max_growth_rate * cell_mass / (1 + growth_inhibition * growth_burden)
1068
+
1069
+ return dxdt
1070
+
1071
+ # Initial conditions (starting with low gene expression and unit cell mass)
1072
+ initial_state = np.zeros(n_genes + 1)
1073
+ initial_state[:n_genes] = 0.1 # Low initial gene expression
1074
+ initial_state[n_genes] = 1.0 # Initial cell mass
1075
+
1076
+ # Solve the ODE system
1077
+ t_span = (0, simulation_time)
1078
+ t_eval = np.linspace(0, simulation_time, time_points)
1079
+
1080
+ solution = solve_ivp(
1081
+ gene_circuit_odes,
1082
+ t_span,
1083
+ initial_state,
1084
+ method="LSODA",
1085
+ t_eval=t_eval,
1086
+ rtol=1e-6,
1087
+ atol=1e-9,
1088
+ )
1089
+
1090
+ # Extract results
1091
+ time_series = solution.t
1092
+ gene_expression_time_series = solution.y[:n_genes, :]
1093
+ cell_growth_time_series = solution.y[n_genes, :]
1094
+
1095
+ # Save results to files
1096
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
1097
+ results_dir = "gene_circuit_results"
1098
+
1099
+ if not os.path.exists(results_dir):
1100
+ os.makedirs(results_dir)
1101
+
1102
+ # Save time series data
1103
+ data_filename = f"{results_dir}/gene_circuit_simulation_{timestamp}.npz"
1104
+ np.savez(
1105
+ data_filename,
1106
+ time=time_series,
1107
+ gene_expression=gene_expression_time_series,
1108
+ cell_growth=cell_growth_time_series,
1109
+ )
1110
+
1111
+ # Save parameters for reference
1112
+ params_filename = f"{results_dir}/simulation_params_{timestamp}.json"
1113
+ params_dict = {
1114
+ "circuit_topology": circuit_topology.tolist(),
1115
+ "kinetic_params": kinetic_params,
1116
+ "growth_params": growth_params,
1117
+ "simulation_time": simulation_time,
1118
+ "time_points": time_points,
1119
+ }
1120
+
1121
+ with open(params_filename, "w") as f:
1122
+ json.dump(params_dict, f, indent=2)
1123
+
1124
+ # Create research log
1125
+ log = f"""Gene Regulatory Circuit Simulation with Growth Feedback - {timestamp}
1126
+
1127
+ SIMULATION SUMMARY:
1128
+ - Simulated a gene regulatory circuit with {n_genes} genes
1129
+ - Incorporated growth feedback mechanisms
1130
+ - Total simulation time: {simulation_time} time units
1131
+ - Collected {time_points} data points
1132
+
1133
+ PARAMETERS:
1134
+ - Basal expression rates: {basal_rates}
1135
+ - Degradation rates: {degradation_rates}
1136
+ - Max growth rate: {max_growth_rate}
1137
+ - Growth inhibition factor: {growth_inhibition}
1138
+
1139
+ RESULTS:
1140
+ - Final gene expression levels: {gene_expression_time_series[:, -1].tolist()}
1141
+ - Final cell mass: {cell_growth_time_series[-1]:.4f}
1142
+ - Growth rate at end of simulation: {max_growth_rate / (1 + growth_inhibition * sum(gene_growth_weights[i] * gene_expression_time_series[i, -1] for i in range(n_genes))):.4f}
1143
+
1144
+ FILES:
1145
+ - Time series data saved to: {data_filename}
1146
+ - Simulation parameters saved to: {params_filename}
1147
+ """
1148
+
1149
+ return log
1150
+
1151
+
1152
+ def identify_fas_functional_domains(sequence, sequence_type="protein", output_file="fas_domains_report.txt"):
1153
+ """Identifies functional domains within a Fatty Acid Synthase (FAS) sequence and predicts their roles.
1154
+
1155
+ Parameters
1156
+ ----------
1157
+ sequence : str
1158
+ The nucleotide or protein sequence of a FAS gene
1159
+ sequence_type : str
1160
+ Type of sequence provided - "protein" or "nucleotide" (default: "protein")
1161
+ output_file : str
1162
+ Name of the output file to save the detailed domain report (default: "fas_domains_report.txt")
1163
+
1164
+ Returns
1165
+ -------
1166
+ str
1167
+ A research log summarizing the steps taken and results of the domain analysis
1168
+
1169
+ """
1170
+ import json
1171
+ import time
1172
+
1173
+ import requests
1174
+ from Bio.Seq import Seq
1175
+
1176
+ research_log = "# Fatty Acid Synthase (FAS) Domain Analysis\n\n"
1177
+ research_log += "## Input Sequence Analysis\n"
1178
+ research_log += f"- Sequence type: {sequence_type}\n"
1179
+ research_log += (
1180
+ f"- Sequence length: {len(sequence)} {'nucleotides' if sequence_type == 'nucleotide' else 'amino acids'}\n\n"
1181
+ )
1182
+
1183
+ # Convert nucleotide to protein if needed
1184
+ if sequence_type == "nucleotide":
1185
+ research_log += "## Sequence Translation\n"
1186
+ research_log += "- Converting nucleotide sequence to protein\n"
1187
+ try:
1188
+ protein_seq = str(Seq(sequence).translate())
1189
+ research_log += f"- Translated protein length: {len(protein_seq)} amino acids\n\n"
1190
+ except Exception as e:
1191
+ research_log += f"- Error in translation: {str(e)}\n"
1192
+ return research_log
1193
+ else:
1194
+ protein_seq = sequence
1195
+
1196
+ # FAS domain information dictionary
1197
+ fas_domains = {
1198
+ "ketoacyl-synt": {
1199
+ "name": "Ketoacyl Synthase (KS)",
1200
+ "function": "Catalyzes the condensation of malonyl-ACP with the growing fatty acid chain",
1201
+ },
1202
+ "Ketoacyl-synt_C": {
1203
+ "name": "Ketoacyl Synthase C-terminal (KS-C)",
1204
+ "function": "C-terminal domain of KS, involved in substrate binding",
1205
+ },
1206
+ "Acyl_transf_1": {
1207
+ "name": "Acyltransferase (AT)",
1208
+ "function": "Transfers acyl groups from acyl-CoA to ACP",
1209
+ },
1210
+ "ketoacyl-red": {
1211
+ "name": "Ketoacyl Reductase (KR)",
1212
+ "function": "Reduces the beta-ketoacyl group to a beta-hydroxyacyl group",
1213
+ },
1214
+ "Thioesterase": {
1215
+ "name": "Thioesterase (TE)",
1216
+ "function": "Releases the fatty acid from ACP by hydrolysis",
1217
+ },
1218
+ "PS-DH": {
1219
+ "name": "Dehydratase (DH)",
1220
+ "function": "Removes water from beta-hydroxyacyl-ACP to form trans-2-enoyl-ACP",
1221
+ },
1222
+ "Enoyl_red": {
1223
+ "name": "Enoyl Reductase (ER)",
1224
+ "function": "Reduces the double bond in enoyl-ACP to a saturated acyl-ACP",
1225
+ },
1226
+ "ACP": {
1227
+ "name": "Acyl Carrier Protein (ACP)",
1228
+ "function": "Carries the growing fatty acid chain between enzyme domains",
1229
+ },
1230
+ "PP-binding": {
1231
+ "name": "Phosphopantetheine Binding Domain",
1232
+ "function": "Attachment site for the phosphopantetheine prosthetic group in ACP",
1233
+ },
1234
+ }
1235
+
1236
+ # HMMER web API for domain identification
1237
+ research_log += "## Domain Identification\n"
1238
+ research_log += "- Using HMMER web API to search against Pfam database\n"
1239
+
1240
+ url = "https://www.ebi.ac.uk/Tools/hmmer/search/hmmscan"
1241
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
1242
+ data = {"hmmdb": "pfam", "seq": protein_seq}
1243
+
1244
+ try:
1245
+ response = requests.post(url, data=json.dumps(data), headers=headers)
1246
+ if response.status_code != 200:
1247
+ research_log += f"- Error: HMMER API returned status code {response.status_code}\n"
1248
+ return research_log
1249
+
1250
+ result_url = response.headers.get("Location", "")
1251
+ if not result_url:
1252
+ research_log += "- Error: No result URL returned from HMMER API\n"
1253
+ return research_log
1254
+
1255
+ # Wait for results to be ready
1256
+ time.sleep(2)
1257
+
1258
+ # Get results
1259
+ result_response = requests.get(f"{result_url}.json")
1260
+ if result_response.status_code != 200:
1261
+ research_log += "- Error: Could not retrieve results from HMMER API\n"
1262
+ return research_log
1263
+
1264
+ results = result_response.json()
1265
+
1266
+ # Process results
1267
+ domains_found = []
1268
+ if "results" in results and "hits" in results["results"]:
1269
+ hits = results["results"]["hits"]
1270
+ for hit in hits:
1271
+ if "domains" in hit:
1272
+ for domain in hit["domains"]:
1273
+ domain_name = hit.get("name", "").split(".")[0]
1274
+ domain_desc = hit.get("desc", "Unknown")
1275
+ domain_start = domain.get("ali_from", 0)
1276
+ domain_end = domain.get("ali_to", 0)
1277
+
1278
+ domains_found.append(
1279
+ {
1280
+ "name": domain_name,
1281
+ "description": domain_desc,
1282
+ "start": domain_start,
1283
+ "end": domain_end,
1284
+ "score": domain.get("score", 0),
1285
+ }
1286
+ )
1287
+
1288
+ # Check for FAS domains
1289
+ research_log += f"- Found {len(domains_found)} domains in total\n\n"
1290
+ research_log += "## FAS Functional Domains Identified\n"
1291
+
1292
+ fas_domains_found = []
1293
+ for domain in domains_found:
1294
+ for fas_key, fas_info in fas_domains.items():
1295
+ if fas_key.lower() in domain["name"].lower():
1296
+ fas_domains_found.append(
1297
+ {
1298
+ "name": fas_info["name"],
1299
+ "pfam_id": domain["name"],
1300
+ "start": domain["start"],
1301
+ "end": domain["end"],
1302
+ "function": fas_info["function"],
1303
+ }
1304
+ )
1305
+
1306
+ if not fas_domains_found:
1307
+ research_log += "- No specific FAS domains identified in the sequence\n"
1308
+ else:
1309
+ research_log += f"- Found {len(fas_domains_found)} FAS-related domains\n"
1310
+ for i, domain in enumerate(fas_domains_found, 1):
1311
+ research_log += f"\n### {i}. {domain['name']} (Positions {domain['start']}-{domain['end']})\n"
1312
+ research_log += f"- Pfam ID: {domain['pfam_id']}\n"
1313
+ research_log += f"- Function: {domain['function']}\n"
1314
+
1315
+ # Save detailed report to file
1316
+ with open(output_file, "w") as f:
1317
+ f.write("# Fatty Acid Synthase (FAS) Domain Analysis Detailed Report\n\n")
1318
+ f.write("## Input Sequence\n")
1319
+ f.write(f"- Type: {sequence_type}\n")
1320
+ f.write(f"- Length: {len(sequence)} {'nucleotides' if sequence_type == 'nucleotide' else 'amino acids'}\n")
1321
+ if sequence_type == "nucleotide":
1322
+ f.write(f"- Translated protein length: {len(protein_seq)} amino acids\n")
1323
+
1324
+ f.write("\n## All Domains Found\n")
1325
+ for i, domain in enumerate(domains_found, 1):
1326
+ f.write(f"\n### {i}. {domain['name']} (Positions {domain['start']}-{domain['end']})\n")
1327
+ f.write(f"- Description: {domain['description']}\n")
1328
+ f.write(f"- Score: {domain['score']}\n")
1329
+
1330
+ f.write("\n## FAS Functional Domains\n")
1331
+ if not fas_domains_found:
1332
+ f.write("- No specific FAS domains identified in the sequence\n")
1333
+ else:
1334
+ for i, domain in enumerate(fas_domains_found, 1):
1335
+ f.write(f"\n### {i}. {domain['name']} (Positions {domain['start']}-{domain['end']})\n")
1336
+ f.write(f"- Pfam ID: {domain['pfam_id']}\n")
1337
+ f.write(f"- Function: {domain['function']}\n")
1338
+
1339
+ research_log += "\n## Summary\n"
1340
+ research_log += f"- Detailed domain analysis saved to: {output_file}\n"
1341
+ if fas_domains_found:
1342
+ research_log += "- The sequence contains multiple domains characteristic of Fatty Acid Synthase\n"
1343
+ research_log += "- The identified domains suggest this sequence is involved in fatty acid biosynthesis\n"
1344
+ else:
1345
+ research_log += "- The sequence does not contain typical FAS domains\n"
1346
+ research_log += "- This may not be a Fatty Acid Synthase gene or may be a partial sequence\n"
1347
+
1348
+ except Exception as e:
1349
+ research_log += f"- Error in domain identification: {str(e)}\n"
1350
+
1351
+ return research_log
Biomni/biomni/tool/systems_biology.py ADDED
@@ -0,0 +1,926 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def query_chatnt(question, sequence, device=-1):
2
+ """
3
+ Call ChatNT to answer a question about a DNA sequence.
4
+
5
+ Parameters:
6
+ -----------
7
+ question : str
8
+ Question to ask about the DNA sequence
9
+ sequence : str
10
+ DNA sequence to analyze
11
+ device : int, optional
12
+ Device to use for the ChatNT model. Default is -1 (CPU).
13
+
14
+ Returns:
15
+ --------
16
+ str
17
+ Answer to the question
18
+ """
19
+ from transformers import pipeline
20
+
21
+ pipe = pipeline(model="InstaDeepAI/ChatNT", trust_remote_code=True, device=device)
22
+
23
+ # Define custom inputs (note that the number of <DNA> token in the english sequence must be equal to len(dna_sequences))
24
+ english_sequence = f"{question} <DNA> ?"
25
+ dna_sequences = [sequence]
26
+
27
+ # Generate sequence
28
+ generated_english_sequence = pipe(inputs={"english_sequence": english_sequence, "dna_sequences": dna_sequences})
29
+
30
+ return generated_english_sequence
31
+
32
+
33
+ def perform_flux_balance_analysis(model_file, constraints=None, objective_reaction=None, output_file="fba_results.csv"):
34
+ """
35
+ Perform Flux Balance Analysis (FBA) on a genome-scale metabolic network model.
36
+
37
+ FBA is a computational technique that predicts metabolic flux distributions
38
+ by formulating and solving a linear optimization problem.
39
+
40
+ Parameters:
41
+ -----------
42
+ model_file : str
43
+ Path to the metabolic model file (SBML or JSON format)
44
+ constraints : dict, optional
45
+ Dictionary of reaction constraints where keys are reaction IDs and
46
+ values are tuples of (lower_bound, upper_bound)
47
+ objective_reaction : str, optional
48
+ Reaction ID to use as the objective function (e.g., biomass reaction)
49
+ If None, uses the model's default objective
50
+ output_file : str, optional
51
+ File name to save the flux distribution results
52
+
53
+ Returns:
54
+ --------
55
+ str
56
+ Research log summarizing the FBA process and results
57
+ """
58
+
59
+ import cobra
60
+ import pandas as pd
61
+
62
+ # Initialize research log
63
+ log = "# Flux Balance Analysis (FBA) Research Log\n\n"
64
+
65
+ # Step 1: Load the metabolic network model
66
+ log += "## Step 1: Loading metabolic model\n"
67
+ try:
68
+ if model_file.endswith(".xml") or model_file.endswith(".sbml"):
69
+ model = cobra.io.read_sbml_model(model_file)
70
+ elif model_file.endswith(".json"):
71
+ model = cobra.io.load_json_model(model_file)
72
+ else:
73
+ model = cobra.io.load_model(model_file)
74
+ log += f"- Successfully loaded model from {model_file}\n"
75
+ log += f"- Model contains {len(model.reactions)} reactions and {len(model.metabolites)} metabolites\n\n"
76
+ except Exception as e:
77
+ log += f"- Error loading model: {str(e)}\n"
78
+ return log
79
+
80
+ # Step 2: Set constraints
81
+ log += "## Step 2: Setting constraints\n"
82
+ if constraints:
83
+ log += "- Applied the following constraints:\n"
84
+ for reaction_id, (lb, ub) in constraints.items():
85
+ try:
86
+ reaction = model.reactions.get_by_id(reaction_id)
87
+ reaction.bounds = (lb, ub)
88
+ log += f" * {reaction_id}: lower_bound={lb}, upper_bound={ub}\n"
89
+ except Exception as e:
90
+ log += f" * Error setting constraint for {reaction_id}: {str(e)}\n"
91
+ else:
92
+ log += "- No additional constraints specified, using model defaults\n"
93
+ log += "\n"
94
+
95
+ # Step 3: Set objective function
96
+ log += "## Step 3: Setting objective function\n"
97
+ if objective_reaction:
98
+ try:
99
+ model.objective = objective_reaction
100
+ log += f"- Set objective function to maximize {objective_reaction}\n\n"
101
+ except Exception as e:
102
+ log += f"- Error setting objective function: {str(e)}\n"
103
+ log += "- Using model's default objective function\n\n"
104
+ else:
105
+ log += f"- Using model's default objective function: {model.objective.expression}\n\n"
106
+
107
+ # Step 4: Solve the FBA problem
108
+ log += "## Step 4: Solving FBA optimization problem\n"
109
+ try:
110
+ solution = model.optimize()
111
+ log += f"- Optimization status: {solution.status}\n"
112
+ log += f"- Objective value: {solution.objective_value:.6f}\n\n"
113
+ except Exception as e:
114
+ log += f"- Error during optimization: {str(e)}\n"
115
+ return log
116
+
117
+ # Step 5: Save and report results
118
+ log += "## Step 5: Analyzing flux distribution\n"
119
+
120
+ # Create a dataframe with the flux distribution
121
+ flux_distribution = pd.DataFrame(
122
+ {
123
+ "reaction_id": [r.id for r in model.reactions],
124
+ "reaction_name": [r.name for r in model.reactions],
125
+ "flux": [solution.fluxes[r.id] for r in model.reactions],
126
+ "lower_bound": [r.lower_bound for r in model.reactions],
127
+ "upper_bound": [r.upper_bound for r in model.reactions],
128
+ }
129
+ )
130
+
131
+ # Save to file
132
+ flux_distribution.to_csv(output_file, index=False)
133
+ log += f"- Flux distribution saved to {output_file}\n"
134
+
135
+ # Report active reactions (non-zero flux)
136
+ active_reactions = flux_distribution[abs(flux_distribution["flux"]) > 1e-6]
137
+ log += f"- Number of active reactions (flux > 1e-6): {len(active_reactions)}\n"
138
+
139
+ # Report top reactions by absolute flux
140
+ top_reactions = flux_distribution.iloc[abs(flux_distribution["flux"]).argsort()[::-1]].head(10)
141
+ log += "- Top 10 reactions by absolute flux magnitude:\n"
142
+ for _, row in top_reactions.iterrows():
143
+ log += f" * {row['reaction_id']} ({row['reaction_name']}): {row['flux']:.6f}\n"
144
+
145
+ log += f"\nFBA analysis complete. Full results available in {output_file}\n"
146
+
147
+ return log
148
+
149
+
150
+ def model_protein_dimerization_network(monomer_concentrations, dimerization_affinities, network_topology):
151
+ """
152
+ Model protein dimerization networks to find equilibrium concentrations of dimers.
153
+
154
+ Parameters:
155
+ -----------
156
+ monomer_concentrations : dict
157
+ Dictionary mapping monomer names to their initial concentrations (in arbitrary units)
158
+ dimerization_affinities : dict
159
+ Dictionary mapping dimer names (as 'A-B' strings) to their association constants (Ka)
160
+ network_topology : list of tuples
161
+ List of (monomer1, monomer2) pairs that can form dimers
162
+
163
+ Returns:
164
+ --------
165
+ str
166
+ Research log summarizing the modeling process and results
167
+ """
168
+ import time
169
+
170
+ import numpy as np
171
+ from scipy.integrate import solve_ivp
172
+
173
+ # Start timing
174
+ start_time = time.time()
175
+
176
+ # Extract unique monomers and create mapping to indices
177
+ all_monomers = list(monomer_concentrations.keys())
178
+ monomer_to_idx = {monomer: i for i, monomer in enumerate(all_monomers)}
179
+
180
+ # Create mapping from dimer indices to names
181
+ dimer_pairs = []
182
+ dimer_names = []
183
+ dimer_affinities = []
184
+
185
+ for m1, m2 in network_topology:
186
+ dimer_name = f"{m1}-{m2}"
187
+ if dimer_name in dimerization_affinities:
188
+ affinity = dimerization_affinities[dimer_name]
189
+ else:
190
+ # Try reverse order
191
+ dimer_name = f"{m2}-{m1}"
192
+ if dimer_name in dimerization_affinities:
193
+ affinity = dimerization_affinities[dimer_name]
194
+ else:
195
+ raise ValueError(f"No affinity found for dimer pair {m1}-{m2}")
196
+
197
+ dimer_pairs.append((monomer_to_idx[m1], monomer_to_idx[m2]))
198
+ dimer_names.append(dimer_name)
199
+ dimer_affinities.append(affinity)
200
+
201
+ # Initial conditions (monomer concentrations followed by dimer concentrations)
202
+ num_monomers = len(all_monomers)
203
+ num_dimers = len(dimer_pairs)
204
+ y0 = np.zeros(num_monomers + num_dimers)
205
+
206
+ for monomer, conc in monomer_concentrations.items():
207
+ y0[monomer_to_idx[monomer]] = conc
208
+
209
+ # Define ODE system
210
+ def dimerization_odes(t, y):
211
+ dydt = np.zeros_like(y)
212
+
213
+ # Extract current concentrations
214
+ monomer_concs = y[:num_monomers]
215
+ dimer_concs = y[num_monomers:]
216
+
217
+ # Calculate rate of change for each species
218
+ for i, ((m1_idx, m2_idx), affinity) in enumerate(zip(dimer_pairs, dimer_affinities, strict=False)):
219
+ # Formation rate: kon * [A] * [B]
220
+ # Dissociation rate: koff * [AB]
221
+ # At equilibrium: kon/koff = Ka (affinity)
222
+ # For simplicity, set kon = Ka and koff = 1
223
+
224
+ kon = affinity
225
+ koff = 1.0
226
+
227
+ # Dimer formation/dissociation
228
+ dimer_idx = num_monomers + i
229
+
230
+ # Rate of change
231
+ formation_rate = kon * monomer_concs[m1_idx] * monomer_concs[m2_idx]
232
+ dissociation_rate = koff * dimer_concs[i]
233
+ net_rate = formation_rate - dissociation_rate
234
+
235
+ # Update rates for monomers and dimers
236
+ dydt[m1_idx] -= net_rate
237
+ dydt[m2_idx] -= net_rate
238
+ dydt[dimer_idx] += net_rate
239
+
240
+ return dydt
241
+
242
+ # Solve ODE system to equilibrium
243
+ # Using a long enough time to reach equilibrium
244
+ t_span = (0, 1000) # Arbitrary long time to reach equilibrium
245
+
246
+ sol = solve_ivp(
247
+ dimerization_odes,
248
+ t_span,
249
+ y0,
250
+ method="BDF", # Good for stiff problems
251
+ rtol=1e-6,
252
+ atol=1e-9,
253
+ )
254
+
255
+ # Extract final concentrations (equilibrium)
256
+ final_monomer_concs = {monomer: sol.y[idx][-1] for monomer, idx in monomer_to_idx.items()}
257
+ final_dimer_concs = {dimer_name: sol.y[num_monomers + i][-1] for i, dimer_name in enumerate(dimer_names)}
258
+
259
+ # Calculate time taken
260
+ elapsed_time = time.time() - start_time
261
+
262
+ # Generate research log
263
+ log = "Protein Dimerization Network Modeling\n"
264
+ log += "=====================================\n\n"
265
+ log += "Network summary:\n"
266
+ log += f"- Number of monomers: {num_monomers}\n"
267
+ log += f"- Number of possible dimers: {num_dimers}\n\n"
268
+
269
+ log += "Initial conditions:\n"
270
+ for monomer, conc in monomer_concentrations.items():
271
+ log += f"- {monomer}: {conc:.4f}\n"
272
+ log += "\n"
273
+
274
+ log += "Dimerization affinities:\n"
275
+ for dimer, affinity in zip(dimer_names, dimer_affinities, strict=False):
276
+ log += f"- {dimer}: {affinity:.4e}\n"
277
+ log += "\n"
278
+
279
+ log += "Equilibrium concentrations:\n"
280
+ log += "Monomers:\n"
281
+ for monomer, conc in final_monomer_concs.items():
282
+ log += f"- {monomer}: {conc:.4f}\n"
283
+
284
+ log += "\nDimers:\n"
285
+ for dimer, conc in final_dimer_concs.items():
286
+ log += f"- {dimer}: {conc:.4f}\n"
287
+ log += "\n"
288
+
289
+ log += f"Simulation completed in {elapsed_time:.2f} seconds.\n"
290
+
291
+ return log
292
+
293
+
294
+ def simulate_metabolic_network_perturbation(
295
+ model_file, initial_concentrations, perturbation_params, simulation_time=100, time_points=1000
296
+ ):
297
+ """
298
+ Construct and simulate kinetic models of metabolic networks and analyze their responses to perturbations.
299
+
300
+ Parameters:
301
+ -----------
302
+ model_file : str
303
+ Path to the COBRA model file (SBML format)
304
+ initial_concentrations : dict
305
+ Dictionary mapping metabolite IDs to their initial concentrations
306
+ perturbation_params : dict
307
+ Dictionary with the following keys:
308
+ - 'time': float, time at which perturbation is applied
309
+ - 'metabolite': str, ID of the metabolite to perturb
310
+ - 'factor': float, multiplication factor for the metabolite concentration
311
+ simulation_time : float, optional
312
+ Total simulation time (default: 100)
313
+ time_points : int, optional
314
+ Number of time points to simulate (default: 1000)
315
+
316
+ Returns:
317
+ --------
318
+ str
319
+ Research log summarizing the steps taken and results obtained
320
+ """
321
+
322
+ import cobra
323
+ import numpy as np
324
+ import pandas as pd
325
+ from scipy.integrate import solve_ivp
326
+
327
+ # Step 1: Load the metabolic model
328
+ try:
329
+ model = cobra.io.read_sbml_model(model_file)
330
+ log = f"Loaded metabolic model from {model_file} with {len(model.reactions)} reactions and {len(model.metabolites)} metabolites.\n\n"
331
+ except Exception as e:
332
+ return f"Error loading model: {str(e)}"
333
+
334
+ # Step 2: Set up metabolite list and initial concentrations
335
+ metabolites = list(model.metabolites)
336
+ metabolite_ids = [m.id for m in metabolites]
337
+
338
+ # Set default initial concentrations for metabolites not specified
339
+ conc_array = np.ones(len(metabolites))
340
+ for i, m_id in enumerate(metabolite_ids):
341
+ if m_id in initial_concentrations:
342
+ conc_array[i] = initial_concentrations[m_id]
343
+
344
+ log += "Initial concentrations set up for all metabolites.\n\n"
345
+
346
+ # Step 3: Define kinetic model using simple mass-action kinetics
347
+ def mass_action_kinetics(reaction, concentrations, metabolite_ids):
348
+ # Simple mass-action kinetics: product of substrate concentrations
349
+ rate = 1.0
350
+ for metabolite, coeff in reaction.metabolites.items():
351
+ if coeff < 0: # Substrate
352
+ idx = metabolite_ids.index(metabolite.id)
353
+ rate *= concentrations[idx] ** abs(coeff)
354
+ return rate
355
+
356
+ # Step 4: Define ODE system
357
+ def ode_system(t, concentrations, metabolite_ids, reactions, perturbation):
358
+ dC_dt = np.zeros(len(concentrations))
359
+
360
+ # Apply perturbation if we're at or past the perturbation time
361
+ if t >= perturbation["time"]:
362
+ perturb_idx = metabolite_ids.index(perturbation["metabolite"])
363
+ if not perturbation.get("applied", False):
364
+ concentrations[perturb_idx] *= perturbation["factor"]
365
+ perturbation["applied"] = True
366
+
367
+ # Calculate reaction rates
368
+ rates = {}
369
+ for reaction in reactions:
370
+ rates[reaction.id] = mass_action_kinetics(reaction, concentrations, metabolite_ids)
371
+
372
+ # Update concentration changes based on stoichiometry and rates
373
+ for i, metabolite_id in enumerate(metabolite_ids):
374
+ for reaction in reactions:
375
+ if metabolite_id in [m.id for m in reaction.metabolites]:
376
+ coeff = reaction.metabolites[model.metabolites.get_by_id(metabolite_id)]
377
+ dC_dt[i] += coeff * rates[reaction.id]
378
+
379
+ return dC_dt
380
+
381
+ log += "Kinetic model defined using mass-action kinetics for all reactions.\n\n"
382
+
383
+ # Step 5: Simulate the system
384
+ perturbation = perturbation_params.copy()
385
+ perturbation["applied"] = False
386
+
387
+ t_span = (0, simulation_time)
388
+ t_eval = np.linspace(0, simulation_time, time_points)
389
+
390
+ log += f"Starting simulation for {simulation_time} time units with perturbation of {perturbation['metabolite']} "
391
+ log += f"by factor {perturbation['factor']} at time {perturbation['time']}.\n\n"
392
+
393
+ try:
394
+ solution = solve_ivp(
395
+ lambda t, y: ode_system(t, y, metabolite_ids, model.reactions, perturbation),
396
+ t_span,
397
+ conc_array,
398
+ t_eval=t_eval,
399
+ method="LSODA",
400
+ )
401
+
402
+ log += f"Simulation completed successfully with {len(solution.t)} time points.\n\n"
403
+ except Exception as e:
404
+ return f"Error during simulation: {str(e)}"
405
+
406
+ # Step 6: Calculate fluxes at each time point
407
+ fluxes = np.zeros((len(solution.t), len(model.reactions)))
408
+ reaction_ids = [r.id for r in model.reactions]
409
+
410
+ for t_idx, t in enumerate(solution.t):
411
+ concentrations = solution.y[:, t_idx]
412
+ perturbation_temp = perturbation.copy()
413
+ if t >= perturbation["time"]:
414
+ perturbation_temp["applied"] = True
415
+
416
+ for r_idx, reaction in enumerate(model.reactions):
417
+ fluxes[t_idx, r_idx] = mass_action_kinetics(reaction, concentrations, metabolite_ids)
418
+
419
+ log += "Calculated reaction fluxes for all time points.\n\n"
420
+
421
+ # Step 7: Save results to files
422
+ conc_df = pd.DataFrame(solution.y.T, columns=metabolite_ids)
423
+ conc_df["time"] = solution.t
424
+ conc_file = "metabolite_concentrations.csv"
425
+ conc_df.to_csv(conc_file, index=False)
426
+
427
+ flux_df = pd.DataFrame(fluxes, columns=reaction_ids)
428
+ flux_df["time"] = solution.t
429
+ flux_file = "reaction_fluxes.csv"
430
+ flux_df.to_csv(flux_file, index=False)
431
+
432
+ log += f"Results saved to {conc_file} and {flux_file}.\n\n"
433
+
434
+ # Step 8: Analyze perturbation response
435
+ perturb_time_idx = np.where(solution.t >= perturbation["time"])[0][0]
436
+ pre_perturb = solution.y[:, perturb_time_idx - 1]
437
+ post_perturb = solution.y[:, perturb_time_idx]
438
+
439
+ # Find metabolites with significant changes
440
+ significant_changes = []
441
+ for i, m_id in enumerate(metabolite_ids):
442
+ rel_change = abs(post_perturb[i] - pre_perturb[i]) / pre_perturb[i] if pre_perturb[i] > 0 else 0
443
+ if rel_change > 0.05: # 5% change threshold
444
+ significant_changes.append((m_id, rel_change))
445
+
446
+ log += "Perturbation Analysis Results:\n"
447
+ log += f"Perturbation of {perturbation['metabolite']} at time {perturbation['time']} by factor {perturbation['factor']}.\n"
448
+ log += f"Number of metabolites with significant immediate response: {len(significant_changes)}.\n"
449
+
450
+ if significant_changes:
451
+ log += "Top 5 most affected metabolites (by relative concentration change):\n"
452
+ for m_id, change in sorted(significant_changes, key=lambda x: x[1], reverse=True)[:5]:
453
+ log += f" - {m_id}: {change * 100:.2f}% change\n"
454
+
455
+ log += "\nSimulation and perturbation analysis completed successfully."
456
+
457
+ return log
458
+
459
+
460
+ def simulate_protein_signaling_network(
461
+ network_structure, reaction_params, species_params, simulation_time=100, time_points=1000
462
+ ):
463
+ """
464
+ Simulate protein signaling network dynamics using ODE-based logic modeling with normalized Hill functions.
465
+
466
+ Parameters:
467
+ -----------
468
+ network_structure : dict
469
+ Dictionary defining the network topology. Each key is a target protein and its value is a list of tuples
470
+ (regulator, regulation_type) where regulation_type is 1 for activation and -1 for inhibition.
471
+
472
+ reaction_params : dict
473
+ Dictionary of reaction parameters. Keys are tuples (regulator, target) and values are dictionaries
474
+ with keys 'W' (weight), 'n' (Hill coefficient), and 'EC50' (half-maximal effective concentration).
475
+
476
+ species_params : dict
477
+ Dictionary of species parameters. Keys are protein names and values are dictionaries
478
+ with keys 'tau' (time constant), 'y0' (initial concentration), and 'ymax' (maximum concentration).
479
+
480
+ simulation_time : float, optional
481
+ Total simulation time in arbitrary time units. Default is 100.
482
+
483
+ time_points : int, optional
484
+ Number of time points for the simulation. Default is 1000.
485
+
486
+ Returns:
487
+ --------
488
+ str
489
+ Research log summarizing the simulation process and results.
490
+ """
491
+ import csv
492
+
493
+ import numpy as np
494
+ from scipy.integrate import solve_ivp
495
+
496
+ # Extract all unique protein species
497
+ all_proteins = set(network_structure.keys())
498
+ for _target, regulators in network_structure.items():
499
+ for regulator, _ in regulators:
500
+ all_proteins.add(regulator)
501
+
502
+ # Create ordered list of proteins for indexing
503
+ proteins = sorted(all_proteins)
504
+ protein_indices = {protein: i for i, protein in enumerate(proteins)}
505
+
506
+ # Define the normalized Hill function
507
+ def hill_function(x, n, ec50):
508
+ return x**n / (x**n + ec50**n)
509
+
510
+ # Define the ODE system
511
+ def ode_system(t, y):
512
+ dydt = np.zeros_like(y)
513
+
514
+ for target, regulators in network_structure.items():
515
+ target_idx = protein_indices[target]
516
+ target_params = species_params[target]
517
+
518
+ # Calculate regulation term for each regulator
519
+ regulation_terms = []
520
+ for regulator, reg_type in regulators:
521
+ regulator_idx = protein_indices[regulator]
522
+ params = reaction_params.get((regulator, target), {})
523
+
524
+ if not params:
525
+ continue
526
+
527
+ x = y[regulator_idx]
528
+ n = params["n"]
529
+ ec50 = params["EC50"]
530
+ weight = params["W"]
531
+
532
+ # Apply Hill function based on regulation type
533
+ if reg_type == 1: # Activation
534
+ term = weight * hill_function(x, n, ec50)
535
+ else: # Inhibition
536
+ term = weight * (1 - hill_function(x, n, ec50))
537
+
538
+ regulation_terms.append(term)
539
+
540
+ # Combine regulation terms (if any)
541
+ if regulation_terms:
542
+ # Simple summation of regulation terms
543
+ regulation = sum(regulation_terms) / len(regulation_terms)
544
+
545
+ # Ensure regulation stays within [0, 1]
546
+ regulation = max(0, min(1, regulation))
547
+
548
+ # Calculate rate of change
549
+ tau = target_params["tau"]
550
+ ymax = target_params["ymax"]
551
+ dydt[target_idx] = (1 / tau) * (regulation * ymax - y[target_idx])
552
+
553
+ return dydt
554
+
555
+ # Set up initial conditions
556
+ y0 = np.zeros(len(proteins))
557
+ for protein, params in species_params.items():
558
+ if protein in protein_indices:
559
+ y0[protein_indices[protein]] = params["y0"]
560
+
561
+ # Set up time points
562
+ t_span = (0, simulation_time)
563
+ t_eval = np.linspace(0, simulation_time, time_points)
564
+
565
+ # Solve the ODE system
566
+ solution = solve_ivp(ode_system, t_span, y0, method="LSODA", t_eval=t_eval, rtol=1e-6, atol=1e-9)
567
+
568
+ # Save results to CSV
569
+ output_file = "protein_signaling_simulation_results.csv"
570
+ with open(output_file, "w", newline="") as f:
571
+ writer = csv.writer(f)
572
+ # Write header
573
+ header = ["Time"] + proteins
574
+ writer.writerow(header)
575
+
576
+ # Write data
577
+ for i in range(len(solution.t)):
578
+ row = [solution.t[i]] + list(solution.y[:, i])
579
+ writer.writerow(row)
580
+
581
+ # Generate research log
582
+ log = "ODE-based Logic Modeling of Protein Signaling Networks\n"
583
+ log += "=============================================\n\n"
584
+ log += f"Network structure: {len(network_structure)} target proteins, {len(proteins)} total proteins\n"
585
+ log += f"Simulation time: {simulation_time} time units\n"
586
+ log += f"Number of time points: {time_points}\n\n"
587
+
588
+ log += "Simulation steps:\n"
589
+ log += "1. Initialized protein concentrations based on provided initial values\n"
590
+ log += "2. Set up ODE system using normalized Hill functions for protein interactions\n"
591
+ log += "3. Solved the ODE system using LSODA integration method\n"
592
+ log += "4. Saved time-series data for all protein concentrations\n\n"
593
+
594
+ log += f"Results saved to: {output_file}\n\n"
595
+
596
+ # Add summary statistics
597
+ log += "Summary of final protein concentrations:\n"
598
+ for i, protein in enumerate(proteins):
599
+ final_conc = solution.y[i, -1]
600
+ log += f"- {protein}: {final_conc:.4f}\n"
601
+
602
+ return log
603
+
604
+
605
+ def compare_protein_structures(pdb_file1, pdb_file2, chain_id1="A", chain_id2="A", output_prefix="protein_comparison"):
606
+ """
607
+ Compares two protein structures to identify structural differences and conformational changes.
608
+
609
+ Parameters:
610
+ -----------
611
+ pdb_file1 : str
612
+ Path to the first PDB file
613
+ pdb_file2 : str
614
+ Path to the second PDB file
615
+ chain_id1 : str, optional
616
+ Chain ID to analyze in the first structure (default: 'A')
617
+ chain_id2 : str, optional
618
+ Chain ID to analyze in the second structure (default: 'A')
619
+ output_prefix : str, optional
620
+ Prefix for output files (default: "protein_comparison")
621
+
622
+ Returns:
623
+ --------
624
+ str
625
+ A research log summarizing the structural comparison analysis
626
+ """
627
+ import warnings
628
+
629
+ import numpy as np
630
+ from Bio.PDB import PDBIO, PDBParser, Select, Superimposer
631
+ from Bio.PDB.PDBExceptions import PDBConstructionWarning
632
+
633
+ # Suppress PDB parsing warnings
634
+ warnings.filterwarnings("ignore", category=PDBConstructionWarning)
635
+
636
+ research_log = []
637
+ research_log.append("# Protein Structure Comparison Analysis\n")
638
+ research_log.append(f"Comparing structures from {pdb_file1} and {pdb_file2}\n")
639
+
640
+ # Initialize parser
641
+ parser = PDBParser()
642
+
643
+ # Parse structures
644
+ research_log.append("## Loading PDB structures")
645
+ structure1 = parser.get_structure("structure1", pdb_file1)
646
+ structure2 = parser.get_structure("structure2", pdb_file2)
647
+
648
+ # Get specified chains
649
+ try:
650
+ chain1 = structure1[0][chain_id1]
651
+ chain2 = structure2[0][chain_id2]
652
+ research_log.append(f"Successfully loaded chain {chain_id1} from {pdb_file1}")
653
+ research_log.append(f"Successfully loaded chain {chain_id2} from {pdb_file2}")
654
+ except KeyError as e:
655
+ return f"Error: Chain not found: {str(e)}"
656
+
657
+ # Get CA atoms for alignment
658
+ ca_atoms1 = []
659
+ ca_atoms2 = []
660
+
661
+ # Create residue mappings based on residue ID
662
+ residues1 = {residue.id[1]: residue for residue in chain1 if residue.has_id("CA")}
663
+ residues2 = {residue.id[1]: residue for residue in chain2 if residue.has_id("CA")}
664
+
665
+ # Find common residues
666
+ common_residues = sorted(set(residues1.keys()) & set(residues2.keys()))
667
+
668
+ if len(common_residues) == 0:
669
+ return "Error: No common residues with CA atoms found between the structures"
670
+
671
+ research_log.append(f"Found {len(common_residues)} common residues for alignment\n")
672
+
673
+ # Get paired atoms for alignment
674
+ for res_id in common_residues:
675
+ ca_atoms1.append(residues1[res_id]["CA"])
676
+ ca_atoms2.append(residues2[res_id]["CA"])
677
+
678
+ # Perform structural alignment
679
+ research_log.append("## Structural Alignment")
680
+ super_imposer = Superimposer()
681
+ super_imposer.set_atoms(ca_atoms1, ca_atoms2)
682
+ super_imposer.apply(structure2.get_atoms())
683
+
684
+ # Calculate RMSD
685
+ rmsd = super_imposer.rms
686
+ research_log.append(f"Overall RMSD: {rmsd:.4f} Å\n")
687
+
688
+ # Calculate per-residue distance after alignment
689
+ research_log.append("## Conformational Differences Analysis")
690
+
691
+ # Find regions with significant differences
692
+ distance_data = []
693
+ significant_changes = []
694
+ threshold = 2.0 # Angstroms threshold for significant difference
695
+
696
+ for res_id in common_residues:
697
+ res1 = residues1[res_id]
698
+ res2 = residues2[res_id]
699
+
700
+ # Calculate distance between CA atoms
701
+ ca1 = res1["CA"]
702
+ ca2 = res2["CA"]
703
+ distance = np.linalg.norm(ca1.coord - ca2.coord)
704
+
705
+ distance_data.append((res_id, distance))
706
+
707
+ if distance > threshold:
708
+ significant_changes.append((res_id, res1.get_resname(), distance))
709
+
710
+ # Save alignment as PDB files
711
+ aligned_file1 = f"{output_prefix}_ref.pdb"
712
+ aligned_file2 = f"{output_prefix}_aligned.pdb"
713
+
714
+ io = PDBIO()
715
+ io.set_structure(structure1)
716
+ io.save(aligned_file1, select=Select())
717
+
718
+ io.set_structure(structure2)
719
+ io.save(aligned_file2, select=Select())
720
+
721
+ research_log.append(f"Aligned structures saved as {aligned_file1} and {aligned_file2}")
722
+
723
+ # Report on significant differences
724
+ if significant_changes:
725
+ research_log.append(
726
+ f"\nIdentified {len(significant_changes)} residues with significant conformational changes:"
727
+ )
728
+ for res_id, res_name, distance in significant_changes:
729
+ research_log.append(f" - Residue {res_name}{res_id}: {distance:.2f} Å displacement")
730
+ else:
731
+ research_log.append("\nNo significant conformational changes detected (threshold: 2.0 Å)")
732
+
733
+ # Save per-residue distance data
734
+ distance_file = f"{output_prefix}_residue_distances.csv"
735
+ with open(distance_file, "w") as f:
736
+ f.write("Residue_ID,Distance(Å)\n")
737
+ for res_id, distance in distance_data:
738
+ f.write(f"{res_id},{distance:.4f}\n")
739
+
740
+ research_log.append(f"\nPer-residue distance data saved to {distance_file}")
741
+
742
+ # Identify regions with continuous changes
743
+ regions = []
744
+ current_region = []
745
+
746
+ for res_id, _, distance in significant_changes:
747
+ if not current_region or res_id == current_region[-1][0] + 1:
748
+ current_region.append((res_id, distance))
749
+ else:
750
+ if len(current_region) >= 3: # Consider regions with at least 3 consecutive residues
751
+ regions.append(current_region)
752
+ current_region = [(res_id, distance)]
753
+
754
+ if current_region and len(current_region) >= 3:
755
+ regions.append(current_region)
756
+
757
+ if regions:
758
+ research_log.append("\n## Continuous Regions with Conformational Changes")
759
+ for i, region in enumerate(regions, 1):
760
+ start_res = region[0][0]
761
+ end_res = region[-1][0]
762
+ avg_dist = sum(r[1] for r in region) / len(region)
763
+ research_log.append(f"Region {i}: Residues {start_res}-{end_res} (Average displacement: {avg_dist:.2f} Å)")
764
+
765
+ research_log.append("\n## Summary")
766
+ research_log.append(f"- Compared structures from {pdb_file1} and {pdb_file2}")
767
+ research_log.append(f"- Overall RMSD: {rmsd:.4f} Å")
768
+ research_log.append(f"- {len(significant_changes)} residues with significant conformational changes")
769
+ research_log.append(f"- {len(regions)} continuous regions of conformational change")
770
+ research_log.append(f"- Files generated: {aligned_file1}, {aligned_file2}, {distance_file}")
771
+
772
+ return "\n".join(research_log)
773
+
774
+
775
+ def simulate_renin_angiotensin_system_dynamics(
776
+ initial_concentrations, rate_constants, feedback_params, simulation_time=48, time_points=100
777
+ ):
778
+ """
779
+ Simulate the time-dependent concentrations of renin-angiotensin system (RAS) components.
780
+
781
+ Parameters:
782
+ -----------
783
+ initial_concentrations : dict
784
+ Initial concentrations of RAS components with keys:
785
+ 'renin', 'angiotensinogen', 'angiotensin_I', 'angiotensin_II',
786
+ 'ACE2_angiotensin_II', 'angiotensin_1_7'
787
+
788
+ rate_constants : dict
789
+ Kinetic rate constants with keys:
790
+ 'k_ren' (renin production), 'k_agt' (angiotensinogen production),
791
+ 'k_ace' (ACE conversion rate), 'k_ace2' (ACE2 conversion rate),
792
+ 'k_at1r' (AT1R binding rate), 'k_mas' (Mas receptor binding rate)
793
+
794
+ feedback_params : dict
795
+ Parameters controlling feedback mechanisms with keys:
796
+ 'fb_ang_II' (angiotensin II feedback), 'fb_ace2' (ACE2 feedback)
797
+
798
+ simulation_time : float, optional
799
+ Total simulation time in hours (default: 48)
800
+
801
+ time_points : int, optional
802
+ Number of time points to evaluate (default: 100)
803
+
804
+ Returns:
805
+ --------
806
+ str
807
+ Research log summarizing the simulation steps and results
808
+ """
809
+
810
+ import numpy as np
811
+ import pandas as pd
812
+ from scipy.integrate import solve_ivp
813
+
814
+ # Extract initial concentrations
815
+ y0 = [
816
+ initial_concentrations["renin"],
817
+ initial_concentrations["angiotensinogen"],
818
+ initial_concentrations["angiotensin_I"],
819
+ initial_concentrations["angiotensin_II"],
820
+ initial_concentrations["ACE2_angiotensin_II"],
821
+ initial_concentrations["angiotensin_1_7"],
822
+ ]
823
+
824
+ # Define the system of ODEs
825
+ def ras_ode_system(t, y):
826
+ renin, agt, ang_I, ang_II, ace2_ang_II, ang_1_7 = y
827
+
828
+ # Production rates with feedback
829
+ renin_production = rate_constants["k_ren"] * (1 / (1 + feedback_params["fb_ang_II"] * ang_II))
830
+ agt_production = rate_constants["k_agt"]
831
+
832
+ # Conversion rates
833
+ ang_I_formation = renin * agt
834
+ ang_II_formation = rate_constants["k_ace"] * ang_I
835
+ ace2_binding = rate_constants["k_ace2"] * ang_II
836
+ ang_1_7_formation = ace2_ang_II
837
+
838
+ # Clearance/degradation (simplified as proportional to concentration)
839
+ renin_clearance = 0.1 * renin
840
+ agt_clearance = 0.05 * agt
841
+ ang_I_clearance = 0.2 * ang_I
842
+ ang_II_clearance = 0.3 * ang_II + rate_constants["k_at1r"] * ang_II
843
+ ace2_ang_II_clearance = 0.15 * ace2_ang_II
844
+ ang_1_7_clearance = 0.25 * ang_1_7 + rate_constants["k_mas"] * ang_1_7
845
+
846
+ # ODEs
847
+ drenin_dt = renin_production - renin_clearance
848
+ dagt_dt = agt_production - agt_clearance - ang_I_formation
849
+ dang_I_dt = ang_I_formation - ang_I_clearance - ang_II_formation
850
+ dang_II_dt = ang_II_formation - ang_II_clearance - ace2_binding
851
+ dace2_ang_II_dt = ace2_binding - ace2_ang_II_clearance - ang_1_7_formation
852
+ dang_1_7_dt = ang_1_7_formation - ang_1_7_clearance
853
+
854
+ return [drenin_dt, dagt_dt, dang_I_dt, dang_II_dt, dace2_ang_II_dt, dang_1_7_dt]
855
+
856
+ # Time points for simulation
857
+ t_span = (0, simulation_time)
858
+ t_eval = np.linspace(0, simulation_time, time_points)
859
+
860
+ # Solve the ODE system
861
+ solution = solve_ivp(ras_ode_system, t_span, y0, method="RK45", t_eval=t_eval, rtol=1e-6)
862
+
863
+ # Create DataFrame with results
864
+ component_names = [
865
+ "Renin",
866
+ "Angiotensinogen",
867
+ "Angiotensin I",
868
+ "Angiotensin II",
869
+ "ACE2-Angiotensin II",
870
+ "Angiotensin 1-7",
871
+ ]
872
+ results_df = pd.DataFrame(solution.y.T, columns=component_names)
873
+ results_df.insert(0, "Time (hours)", solution.t)
874
+
875
+ # Save results to CSV
876
+ results_file = "ras_simulation_results.csv"
877
+ results_df.to_csv(results_file, index=False)
878
+
879
+ # Create research log
880
+ log = f"""RAS ODE Modeling Simulation Log:
881
+
882
+ 1. Initialized RAS component concentrations:
883
+ - Renin: {initial_concentrations["renin"]}
884
+ - Angiotensinogen: {initial_concentrations["angiotensinogen"]}
885
+ - Angiotensin I: {initial_concentrations["angiotensin_I"]}
886
+ - Angiotensin II: {initial_concentrations["angiotensin_II"]}
887
+ - ACE2-Angiotensin II complex: {initial_concentrations["ACE2_angiotensin_II"]}
888
+ - Angiotensin 1-7: {initial_concentrations["angiotensin_1_7"]}
889
+
890
+ 2. Applied rate constants:
891
+ - Renin production (k_ren): {rate_constants["k_ren"]}
892
+ - Angiotensinogen production (k_agt): {rate_constants["k_agt"]}
893
+ - ACE conversion rate (k_ace): {rate_constants["k_ace"]}
894
+ - ACE2 conversion rate (k_ace2): {rate_constants["k_ace2"]}
895
+ - AT1R binding rate (k_at1r): {rate_constants["k_at1r"]}
896
+ - Mas receptor binding rate (k_mas): {rate_constants["k_mas"]}
897
+
898
+ 3. Feedback parameters:
899
+ - Angiotensin II feedback (fb_ang_II): {feedback_params["fb_ang_II"]}
900
+ - ACE2 feedback (fb_ace2): {feedback_params["fb_ace2"]}
901
+
902
+ 4. Simulation parameters:
903
+ - Total simulation time: {simulation_time} hours
904
+ - Number of time points: {time_points}
905
+
906
+ 5. Solved system of ODEs using SciPy's solve_ivp with RK45 method.
907
+
908
+ 6. Final concentrations at {simulation_time} hours:
909
+ - Renin: {results_df["Renin"].iloc[-1]:.4f}
910
+ - Angiotensinogen: {results_df["Angiotensinogen"].iloc[-1]:.4f}
911
+ - Angiotensin I: {results_df["Angiotensin I"].iloc[-1]:.4f}
912
+ - Angiotensin II: {results_df["Angiotensin II"].iloc[-1]:.4f}
913
+ - ACE2-Angiotensin II complex: {results_df["ACE2-Angiotensin II"].iloc[-1]:.4f}
914
+ - Angiotensin 1-7: {results_df["Angiotensin 1-7"].iloc[-1]:.4f}
915
+
916
+ 7. Results saved to file: {results_file}
917
+
918
+ 8. Key observations:
919
+ - The model captures the conversion of Angiotensinogen to Angiotensin I via Renin
920
+ - Angiotensin I is converted to Angiotensin II via ACE
921
+ - Angiotensin II binds with ACE2 to form the ACE2-Angiotensin II complex
922
+ - The complex is converted to Angiotensin 1-7
923
+ - Feedback mechanisms regulate the production rates
924
+ """
925
+
926
+ return log
Biomni/biomni/tool/tool_registry.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pickle
2
+
3
+ import pandas as pd
4
+
5
+
6
+ class ToolRegistry:
7
+ def __init__(self, tools):
8
+ self.tools = []
9
+ self.next_id = 0
10
+
11
+ for j in tools.values():
12
+ for tool in j:
13
+ self.register_tool(tool)
14
+
15
+ docs = []
16
+ for tool_id in range(len(self.tools)):
17
+ docs.append([int(tool_id), self.get_tool_by_id(int(tool_id))])
18
+ self.document_df = pd.DataFrame(docs, columns=["docid", "document_content"])
19
+
20
+ # self.langchain_tools = {}
21
+ # for module, api_list in tools.items():
22
+ # self.langchain_tools.update({self.get_id_by_name(api['name']): api_schema_to_langchain_tool(api, mode = 'custom_tool', module_name = module) for api in api_list})
23
+
24
+ def register_tool(self, tool):
25
+ if self.validate_tool(tool):
26
+ tool["id"] = self.next_id
27
+ self.tools.append(tool)
28
+ self.next_id += 1
29
+ else:
30
+ raise ValueError("Invalid tool format")
31
+
32
+ def validate_tool(self, tool):
33
+ required_keys = ["name", "description", "required_parameters"]
34
+ return all(key in tool for key in required_keys)
35
+
36
+ def get_tool_by_name(self, name):
37
+ for tool in self.tools:
38
+ if tool["name"] == name:
39
+ return tool
40
+ return None
41
+
42
+ def get_tool_by_id(self, tool_id):
43
+ for tool in self.tools:
44
+ if tool["id"] == tool_id:
45
+ return tool
46
+ return None
47
+
48
+ def get_id_by_name(self, name):
49
+ for tool in self.tools:
50
+ if tool["name"] == name:
51
+ return tool["id"]
52
+ return None
53
+
54
+ def get_name_by_id(self, tool_id):
55
+ for tool in self.tools:
56
+ if tool["id"] == tool_id:
57
+ return tool["name"]
58
+ return None
59
+
60
+ def list_tools(self):
61
+ return [{"name": tool["name"], "id": tool["id"]} for tool in self.tools]
62
+
63
+ def remove_tool_by_id(self, tool_id):
64
+ # Remove the tool with the given id
65
+ tool = self.get_tool_by_id(tool_id)
66
+ if tool:
67
+ self.tools = [t for t in self.tools if t["id"] != tool_id]
68
+ return True
69
+ return False
70
+
71
+ def remove_tool_by_name(self, name):
72
+ # Remove the tool with the given name
73
+ tool = self.get_tool_by_name(name)
74
+ if tool:
75
+ self.tools = [t for t in self.tools if t["name"] != name]
76
+ return True
77
+ return False
78
+
79
+ def save_registry(self, filename):
80
+ with open(filename, "wb") as file:
81
+ pickle.dump(self, file)
82
+
83
+ # def get_langchain_tool_by_id(self, id):
84
+ # return self.langchain_tools[id]
85
+
86
+ @staticmethod
87
+ def load_registry(filename):
88
+ with open(filename, "rb") as file:
89
+ return pickle.load(file)
Biomni/biomni/utils.py ADDED
@@ -0,0 +1,2469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import enum
3
+ import importlib
4
+ import json
5
+ import os
6
+ import pickle
7
+ import subprocess
8
+ import tempfile
9
+ import traceback
10
+ import zipfile
11
+ from typing import Any, ClassVar
12
+ from urllib.parse import urljoin
13
+
14
+ import pandas as pd
15
+ import requests
16
+ import tqdm # Add tqdm for progress bar
17
+ from langchain_core.callbacks import BaseCallbackHandler
18
+ from langchain_core.messages.base import get_msg_title_repr
19
+ from langchain_core.tools import StructuredTool
20
+ from langchain_core.utils.interactive_env import is_interactive_env
21
+ from pydantic import BaseModel, Field, ValidationError
22
+
23
+
24
+ # Add these new functions for running R code and CLI commands
25
+ def run_r_code(code: str) -> str:
26
+ """Run R code using subprocess.
27
+
28
+ Args:
29
+ code: R code to run
30
+
31
+ Returns:
32
+ Output of the R code
33
+
34
+ """
35
+ try:
36
+ # Create a temporary file to store the R code
37
+ with tempfile.NamedTemporaryFile(suffix=".R", mode="w", delete=False) as f:
38
+ f.write(code)
39
+ temp_file = f.name
40
+
41
+ # Run the R code using Rscript
42
+ result = subprocess.run(["Rscript", temp_file], capture_output=True, text=True, check=False)
43
+
44
+ # Clean up the temporary file
45
+ os.unlink(temp_file)
46
+
47
+ # Return the output
48
+ if result.returncode != 0:
49
+ return f"Error running R code:\n{result.stderr}"
50
+ else:
51
+ return result.stdout
52
+ except Exception as e:
53
+ return f"Error running R code: {str(e)}"
54
+
55
+
56
+ def run_bash_script(script: str) -> str:
57
+ """Run a Bash script using subprocess.
58
+
59
+ Args:
60
+ script: Bash script to run
61
+
62
+ Returns:
63
+ Output of the Bash script
64
+
65
+ Example:
66
+ This is how to use the function
67
+
68
+ .. code-block:: python
69
+
70
+ # Example of a complex Bash script
71
+ script = '''
72
+ #!/bin/bash
73
+
74
+ # Define variables
75
+ DATA_DIR="/path/to/data"
76
+ OUTPUT_FILE="results.txt"
77
+
78
+ # Create output directory if it doesn't exist
79
+ mkdir -p $(dirname $OUTPUT_FILE)
80
+
81
+ # Loop through files
82
+ for file in $DATA_DIR/*.txt; do
83
+ echo "Processing $file..."
84
+ # Count lines in each file
85
+ line_count=$(wc -l < $file)
86
+ echo "$file: $line_count lines" >> $OUTPUT_FILE
87
+ done
88
+
89
+ echo "Processing complete. Results saved to $OUTPUT_FILE"
90
+ '''
91
+ result = run_bash_script(script)
92
+ print(result)
93
+
94
+ """
95
+ try:
96
+ # Trim any leading/trailing whitespace
97
+ script = script.strip()
98
+
99
+ # If the script is empty, return an error
100
+ if not script:
101
+ return "Error: Empty script"
102
+
103
+ # Create a temporary file to store the Bash script
104
+ with tempfile.NamedTemporaryFile(suffix=".sh", mode="w", delete=False) as f:
105
+ # Add shebang if not present
106
+ if not script.startswith("#!/"):
107
+ f.write("#!/bin/bash\n")
108
+ # Add set -e to exit on error
109
+ if "set -e" not in script:
110
+ f.write("set -e\n")
111
+ f.write(script)
112
+ temp_file = f.name
113
+
114
+ # Make the script executable
115
+ os.chmod(temp_file, 0o755)
116
+
117
+ # Get current environment variables and working directory
118
+ env = os.environ.copy()
119
+ cwd = os.getcwd()
120
+
121
+ # Run the Bash script with the current environment and working directory
122
+ result = subprocess.run(
123
+ [temp_file],
124
+ shell=True,
125
+ capture_output=True,
126
+ text=True,
127
+ check=False,
128
+ env=env,
129
+ cwd=cwd,
130
+ )
131
+
132
+ # Clean up the temporary file
133
+ os.unlink(temp_file)
134
+
135
+ # Return the output
136
+ if result.returncode != 0:
137
+ traceback.print_stack()
138
+ print(result)
139
+ return f"Error running Bash script (exit code {result.returncode}):\n{result.stderr}"
140
+ else:
141
+ return result.stdout
142
+ except Exception as e:
143
+ traceback.print_exc()
144
+ return f"Error running Bash script: {str(e)}"
145
+
146
+
147
+ # Keep the run_cli_command for backward compatibility
148
+ def run_cli_command(command: str) -> str:
149
+ """Run a CLI command using subprocess.
150
+
151
+ Args:
152
+ command: CLI command to run
153
+
154
+ Returns:
155
+ Output of the CLI command
156
+
157
+ """
158
+ try:
159
+ # Trim any leading/trailing whitespace
160
+ command = command.strip()
161
+
162
+ # If the command is empty, return an error
163
+ if not command:
164
+ return "Error: Empty command"
165
+
166
+ # Split the command into a list of arguments, handling quoted arguments correctly
167
+ import shlex
168
+
169
+ args = shlex.split(command)
170
+
171
+ # Run the command
172
+ result = subprocess.run(args, capture_output=True, text=True, check=False)
173
+
174
+ # Return the output
175
+ if result.returncode != 0:
176
+ return f"Error running command '{command}':\n{result.stderr}"
177
+ else:
178
+ return result.stdout
179
+ except Exception as e:
180
+ return f"Error running command '{command}': {str(e)}"
181
+
182
+
183
+ def run_with_timeout(func, args=None, kwargs=None, timeout=600):
184
+ """Run a function with a timeout using threading instead of multiprocessing.
185
+ This allows variables to persist in the global namespace between function calls.
186
+ Returns the function result or a timeout error message.
187
+ """
188
+ if args is None:
189
+ args = []
190
+ if kwargs is None:
191
+ kwargs = {}
192
+
193
+ import ctypes
194
+ import queue
195
+ import threading
196
+
197
+ result_queue = queue.Queue()
198
+
199
+ def thread_func(func, args, kwargs, result_queue):
200
+ """Function to run in a separate thread."""
201
+ try:
202
+ result = func(*args, **kwargs)
203
+ result_queue.put(("success", result))
204
+ except Exception as e:
205
+ result_queue.put(("error", str(e)))
206
+
207
+ # Start a separate thread
208
+ thread = threading.Thread(target=thread_func, args=(func, args, kwargs, result_queue))
209
+ thread.daemon = True # Set as daemon so it will be killed when main thread exits
210
+ thread.start()
211
+
212
+ # Wait for the specified timeout
213
+ thread.join(timeout)
214
+
215
+ # Check if the thread is still running after timeout
216
+ if thread.is_alive():
217
+ print(f"TIMEOUT: Code execution timed out after {timeout} seconds")
218
+
219
+ # Unfortunately, there's no clean way to force terminate a thread in Python
220
+ # The recommended approach is to use daemon threads and let them be killed when main thread exits
221
+ # Here, we'll try to raise an exception in the thread to make it stop
222
+ try:
223
+ # Get thread ID and try to terminate it
224
+ thread_id = thread.ident
225
+ if thread_id:
226
+ # This is a bit dangerous and not 100% reliable
227
+ # It attempts to raise a SystemExit exception in the thread
228
+ res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), ctypes.py_object(SystemExit))
229
+ if res > 1:
230
+ # Oops, we raised too many exceptions
231
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), None)
232
+ except Exception as e:
233
+ print(f"Error trying to terminate thread: {e}")
234
+
235
+ return f"ERROR: Code execution timed out after {timeout} seconds. Please try with simpler inputs or break your task into smaller steps."
236
+
237
+ # Get the result from the queue if available
238
+ try:
239
+ status, result = result_queue.get(block=False)
240
+ return result if status == "success" else f"Error in execution: {result}"
241
+ except queue.Empty:
242
+ return "Error: Execution completed but no result was returned"
243
+
244
+
245
+ class api_schema(BaseModel):
246
+ """api schema specification."""
247
+
248
+ api_schema: str | None = Field(description="The api schema as a dictionary")
249
+
250
+
251
+ def function_to_api_schema(function_string, llm):
252
+ prompt = """
253
+ Based on a code snippet and help me write an API docstring in the format like this:
254
+
255
+ {{'name': 'get_gene_set_enrichment',
256
+ 'description': 'Given a list of genes, identify a pathway that is enriched for this gene set. Return a list of pathway name, p-value, z-scores.',
257
+ 'required_parameters': [{{'name': 'genes',
258
+ 'type': 'List[str]',
259
+ 'description': 'List of g`ene symbols to analyze',
260
+ 'default': None}}],
261
+ 'optional_parameters': [{{'name': 'top_k',
262
+ 'type': 'int',
263
+ 'description': 'Top K pathways to return',
264
+ 'default': 10}}, {{'name': 'database',
265
+ 'type': 'str',
266
+ 'description': 'Name of the database to use for enrichment analysis',
267
+ 'default': "gene_ontology"}}]}}
268
+
269
+ Strictly follow the input from the function - don't create fake optional parameters.
270
+ For variable without default values, set them as None, not null.
271
+ For variable with boolean values, use capitalized True or False, not true or false.
272
+ Do not add any return type in the docstring.
273
+ Be as clear and succint as possible for the descriptions. Please do not make it overly verbose.
274
+ Here is the code snippet:
275
+ {code}
276
+ """
277
+ llm = llm.with_structured_output(api_schema)
278
+
279
+ for _ in range(7):
280
+ try:
281
+ api = llm.invoke(prompt.format(code=function_string)).dict()["api_schema"]
282
+ return ast.literal_eval(api) # -> prefer "default": None
283
+ # return json.loads(api) # -> prefer "default": null
284
+ except Exception as e:
285
+ print("API string:", api)
286
+ print("Error parsing the API string:", e)
287
+ continue
288
+
289
+ return "Error: Could not parse the API schema"
290
+ # return
291
+
292
+
293
+ def get_all_functions_from_file(file_path):
294
+ with open(file_path) as file:
295
+ file_content = file.read()
296
+
297
+ # Parse the file content into an AST (Abstract Syntax Tree)
298
+ tree = ast.parse(file_content)
299
+
300
+ # List to hold the top-level functions as strings
301
+ functions = []
302
+
303
+ # Walk through the AST nodes
304
+ for node in tree.body: # Only consider top-level nodes in the body
305
+ if isinstance(node, ast.FunctionDef): # Check if the node is a function definition
306
+ # Skip if function name starts with underscore
307
+ if node.name.startswith("_"):
308
+ continue
309
+
310
+ start_line = node.lineno - 1 # Get the starting line of the function
311
+ end_line = node.end_lineno # Get the ending line of the function (only available in Python 3.8+)
312
+ func_code = file_content.splitlines()[start_line:end_line]
313
+ functions.append("\n".join(func_code)) # Join lines of the function and add to the list
314
+
315
+ return functions
316
+
317
+
318
+ def write_python_code(request: str):
319
+ from langchain_anthropic import ChatAnthropic
320
+ from langchain_core.output_parsers import StrOutputParser
321
+ from langchain_core.prompts import ChatPromptTemplate
322
+
323
+ model = ChatAnthropic(model="claude-3-5-sonnet-20240620")
324
+ template = """Write some python code to solve the user's problem.
325
+
326
+ Return only python code in Markdown format, e.g.:
327
+
328
+ ```python
329
+ ....
330
+ ```"""
331
+ prompt = ChatPromptTemplate.from_messages([("system", template), ("human", "{input}")])
332
+
333
+ def _sanitize_output(text: str):
334
+ _, after = text.split("```python")
335
+ return after.split("```")[0]
336
+
337
+ chain = prompt | model | StrOutputParser() | _sanitize_output
338
+ return chain.invoke({"input": "write a code that " + request})
339
+
340
+
341
+ def execute_graphql_query(
342
+ query: str,
343
+ variables: dict,
344
+ api_address: str = "https://api.genetics.opentargets.org/graphql",
345
+ ) -> dict:
346
+ """Executes a GraphQL query with variables and returns the data as a dictionary."""
347
+ headers = {"Content-Type": "application/json"}
348
+ response = requests.post(api_address, json={"query": query, "variables": variables}, headers=headers)
349
+ if response.status_code == 200:
350
+ return response.json()
351
+ else:
352
+ print(response.text)
353
+ response.raise_for_status()
354
+
355
+
356
+ def get_tool_decorated_functions(relative_path):
357
+ import ast
358
+ import importlib.util
359
+ import os
360
+
361
+ # Get the directory of the current file (__init__.py)
362
+ current_dir = os.path.dirname(os.path.abspath(__file__))
363
+
364
+ # Construct the absolute path from the relative path
365
+ file_path = os.path.join(current_dir, relative_path)
366
+
367
+ with open(file_path) as file:
368
+ tree = ast.parse(file.read(), filename=file_path)
369
+
370
+ tool_function_names = []
371
+
372
+ for node in ast.walk(tree):
373
+ if isinstance(node, ast.FunctionDef):
374
+ for decorator in node.decorator_list:
375
+ if (
376
+ isinstance(decorator, ast.Name)
377
+ and decorator.id == "tool"
378
+ or (
379
+ isinstance(decorator, ast.Call)
380
+ and isinstance(decorator.func, ast.Name)
381
+ and decorator.func.id == "tool"
382
+ )
383
+ ):
384
+ tool_function_names.append(node.name)
385
+
386
+ # Calculate the module name from the relative path
387
+ package_path = os.path.relpath(file_path, start=current_dir)
388
+ module_name = package_path.replace(os.path.sep, ".").rsplit(".", 1)[0]
389
+
390
+ # Import the module and get the function objects
391
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
392
+ module = importlib.util.module_from_spec(spec)
393
+ spec.loader.exec_module(module)
394
+
395
+ tool_functions = [getattr(module, name) for name in tool_function_names]
396
+
397
+ return tool_functions
398
+
399
+
400
+ def process_bio_retrieval_ducoment(documents_df):
401
+ ir_corpus = {}
402
+ corpus2tool = {}
403
+ for row in documents_df.itertuples():
404
+ doc = row.document_content
405
+ ir_corpus[row.docid] = (
406
+ (doc.get("name", "") or "")
407
+ + ", "
408
+ + (doc.get("description", "") or "")
409
+ + ", "
410
+ + (doc.get("url", "") or "")
411
+ + ", "
412
+ + ", required_params: "
413
+ + json.dumps(doc.get("required_parameters", ""))
414
+ + ", optional_params: "
415
+ + json.dumps(doc.get("optional_parameters", ""))
416
+ )
417
+
418
+ corpus2tool[
419
+ (doc.get("name", "") or "")
420
+ + ", "
421
+ + (doc.get("description", "") or "")
422
+ + ", "
423
+ + (doc.get("url", "") or "")
424
+ + ", "
425
+ + ", required_params: "
426
+ + json.dumps(doc.get("required_parameters", ""))
427
+ + ", optional_params: "
428
+ + json.dumps(doc.get("optional_parameters", ""))
429
+ ] = doc["name"]
430
+ return ir_corpus, corpus2tool
431
+
432
+
433
+ def load_pickle(file):
434
+ import pickle
435
+
436
+ with open(file, "rb") as f:
437
+ return pickle.load(f)
438
+
439
+
440
+ def pretty_print(message, printout=True):
441
+ if isinstance(message, tuple):
442
+ title = message
443
+ elif isinstance(message.content, list):
444
+ title = get_msg_title_repr(message.type.title().upper() + " Message", bold=is_interactive_env())
445
+ if message.name is not None:
446
+ title += f"\nName: {message.name}"
447
+
448
+ for i in message.content:
449
+ if i["type"] == "text":
450
+ title += f"\n{i['text']}\n"
451
+ elif i["type"] == "tool_use":
452
+ title += f"\nTool: {i['name']}"
453
+ title += f"\nInput: {i['input']}"
454
+ if printout:
455
+ print(f"{title}")
456
+ else:
457
+ title = get_msg_title_repr(message.type.title() + " Message", bold=is_interactive_env())
458
+ if message.name is not None:
459
+ title += f"\nName: {message.name}"
460
+ title += f"\n\n{message.content}"
461
+ if printout:
462
+ print(f"{title}")
463
+ return title
464
+
465
+
466
+ class CustomBaseModel(BaseModel):
467
+ api_schema: ClassVar[dict] = None # Class variable to store api_schema
468
+
469
+ # Add model_config with arbitrary_types_allowed=True
470
+ model_config = {"arbitrary_types_allowed": True}
471
+
472
+ @classmethod
473
+ def set_api_schema(cls, schema: dict):
474
+ cls.api_schema = schema
475
+
476
+ @classmethod
477
+ def model_validate(cls, obj):
478
+ try:
479
+ return super().model_validate(obj)
480
+ except (ValidationError, AttributeError) as e:
481
+ if not cls.api_schema:
482
+ raise e # If no api_schema is set, raise original error
483
+
484
+ error_msg = "Required Parameters:\n"
485
+ for param in cls.api_schema["required_parameters"]:
486
+ error_msg += f"- {param['name']} ({param['type']}): {param['description']}\n"
487
+
488
+ error_msg += "\nErrors:\n"
489
+ for err in e.errors():
490
+ field = err["loc"][0] if err["loc"] else "input"
491
+ error_msg += f"- {field}: {err['msg']}\n"
492
+
493
+ if not obj:
494
+ error_msg += "\nNo input provided"
495
+ else:
496
+ error_msg += "\nProvided Input:\n"
497
+ for key, value in obj.items():
498
+ error_msg += f"- {key}: {value}\n"
499
+
500
+ missing_params = {param["name"] for param in cls.api_schema["required_parameters"]} - set(obj.keys())
501
+ if missing_params:
502
+ error_msg += "\nMissing Parameters:\n"
503
+ for param in missing_params:
504
+ error_msg += f"- {param}\n"
505
+
506
+ # # Create proper validation error structure
507
+ raise ValidationError.from_exception_data(
508
+ title="Validation Error",
509
+ line_errors=[
510
+ {
511
+ "type": "value_error",
512
+ "loc": ("input",),
513
+ "input": obj,
514
+ "ctx": {
515
+ "error": error_msg,
516
+ },
517
+ }
518
+ ],
519
+ ) from None
520
+
521
+
522
+ def safe_execute_decorator(func):
523
+ def wrapper(*args, **kwargs):
524
+ try:
525
+ return func(*args, **kwargs)
526
+ except Exception as e:
527
+ return str(e)
528
+
529
+ return wrapper
530
+
531
+
532
+ def api_schema_to_langchain_tool(api_schema, mode="generated_tool", module_name=None):
533
+ if mode == "generated_tool":
534
+ module = importlib.import_module("biomni.tool.generated_tool." + api_schema["tool_name"] + ".api")
535
+ elif mode == "custom_tool":
536
+ module = importlib.import_module(module_name)
537
+
538
+ api_function = getattr(module, api_schema["name"])
539
+ api_function = safe_execute_decorator(api_function)
540
+
541
+ # Define a mapping from string type names to actual Python type objects
542
+ type_mapping = {
543
+ "string": str,
544
+ "integer": int,
545
+ "boolean": bool,
546
+ "pandas": pd.DataFrame, # Use the imported pandas.DataFrame directly
547
+ "str": str,
548
+ "int": int,
549
+ "bool": bool,
550
+ "List[str]": list[str],
551
+ "List[int]": list[int],
552
+ "Dict": dict,
553
+ "Any": Any,
554
+ }
555
+
556
+ # Create the fields and annotations
557
+ annotations = {}
558
+ for param in api_schema["required_parameters"]:
559
+ param_type = param["type"]
560
+ if param_type in type_mapping:
561
+ annotations[param["name"]] = type_mapping[param_type]
562
+ else:
563
+ # For types not in the mapping, try a safer approach than direct eval
564
+ try:
565
+ annotations[param["name"]] = eval(param_type)
566
+ except (NameError, SyntaxError):
567
+ # Default to Any for unknown types
568
+ annotations[param["name"]] = Any
569
+
570
+ fields = {param["name"]: Field(description=param["description"]) for param in api_schema["required_parameters"]}
571
+
572
+ # Create the ApiInput class dynamically
573
+ ApiInput = type("Input", (CustomBaseModel,), {"__annotations__": annotations, **fields})
574
+ # Set the api_schema
575
+ ApiInput.set_api_schema(api_schema)
576
+
577
+ # Create the StructuredTool
578
+ api_tool = StructuredTool.from_function(
579
+ func=api_function,
580
+ name=api_schema["name"],
581
+ description=api_schema["description"],
582
+ args_schema=ApiInput,
583
+ return_direct=True,
584
+ )
585
+
586
+ return api_tool
587
+
588
+
589
+ class ID(enum.Enum):
590
+ ENTREZ = "Entrez"
591
+ ENSEMBL = "Ensembl without version" # e.g. ENSG00000123374
592
+ ENSEMBL_W_VERSION = "Ensembl with version" # e.g. ENSG00000123374.10 (needed for GTEx)
593
+
594
+
595
+ def get_gene_id(gene_symbol: str, id_type: ID):
596
+ """Get the ID for a gene symbol. If no match found, returns None."""
597
+ if id_type == ID.ENTREZ:
598
+ return _get_gene_id_entrez(gene_symbol)
599
+ elif id_type == ID.ENSEMBL:
600
+ return _get_gene_id_ensembl(gene_symbol)
601
+ elif id_type == ID.ENSEMBL_W_VERSION:
602
+ return _get_gene_id_ensembl_with_version(gene_symbol)
603
+ else:
604
+ raise ValueError(f"ID type {id_type} not supported")
605
+
606
+
607
+ def _get_gene_id_entrez(gene_symbol: str):
608
+ """Get the Entrez ID for a gene symbol. If no match found, returns None
609
+ e.g. 1017 (CDK2).
610
+ """
611
+ api_call = f"https://mygene.info/v3/query?species=human&q=symbol:{gene_symbol}"
612
+ response = requests.get(api_call)
613
+ response_json = response.json()
614
+
615
+ if len(response_json["hits"]) == 0:
616
+ return None
617
+ else:
618
+ return response_json["hits"][0]["entrezgene"]
619
+
620
+
621
+ def _get_gene_id_ensembl(gene_symbol):
622
+ """Get the Ensembl ID for a gene symbol. If no match found, returns None
623
+ e.g. ENSG00000123374.
624
+ """
625
+ api_call = f"https://mygene.info/v3/query?species=human&fields=ensembl&q=symbol:{gene_symbol}"
626
+ response = requests.get(api_call)
627
+ response_json = response.json()
628
+
629
+ if len(response_json["hits"]) == 0:
630
+ return None
631
+ else:
632
+ ensembl = response_json["hits"][0]["ensembl"]
633
+ if isinstance(ensembl, list):
634
+ return ensembl[0][
635
+ "gene"
636
+ ] # Sometimes returns a list, for example RNH1 (first elem is on chr11, second is on scaffold_hschr11)
637
+ else:
638
+ return ensembl["gene"]
639
+
640
+
641
+ def _get_gene_id_ensembl_with_version(gene_symbol):
642
+ """Get the Ensembl ID for a gene symbol. If no match found, returns None
643
+ e.g. ENSG00000123374.10.
644
+ """
645
+ api_base = "https://gtexportal.org/api/v2/reference/gene"
646
+ params = {"geneId": gene_symbol}
647
+ response_json = requests.get(api_base, params=params).json()
648
+
649
+ if len(response_json["data"]) == 0:
650
+ return None
651
+ else:
652
+ return response_json["data"][0]["gencodeId"]
653
+
654
+
655
+ def save_pkl(f, filename):
656
+ with open(filename, "wb") as file:
657
+ pickle.dump(f, file)
658
+
659
+
660
+ def load_pkl(filename):
661
+ with open(filename, "rb") as file:
662
+ return pickle.load(file)
663
+
664
+
665
+ _TEXT_COLOR_MAPPING = {
666
+ "blue": "36;1",
667
+ "yellow": "33;1",
668
+ "pink": "38;5;200",
669
+ "green": "32;1",
670
+ "red": "31;1",
671
+ }
672
+
673
+
674
+ def color_print(text, color="blue"):
675
+ color_str = _TEXT_COLOR_MAPPING[color]
676
+ print(f"\u001b[{color_str}m\033[1;3m{text}\u001b[0m")
677
+
678
+
679
+ class PromptLogger(BaseCallbackHandler):
680
+ def on_chat_model_start(self, serialized, messages, **kwargs):
681
+ for message in messages[0]:
682
+ color_print(message.pretty_repr(), color="green")
683
+
684
+
685
+ class NodeLogger(BaseCallbackHandler):
686
+ def on_llm_end(self, response, **kwargs): # response of type LLMResult
687
+ for generations in response.generations: # response.generations of type List[List[Generations]] becuase "each input could have multiple candidate generations"
688
+ for generation in generations:
689
+ generated_text = generation.message.content
690
+ # token_usage = generation.message.response_metadata["token_usage"]
691
+ color_print(generated_text, color="yellow")
692
+
693
+ def on_agent_action(self, action, **kwargs):
694
+ color_print(action.log, color="pink")
695
+
696
+ def on_agent_finish(self, finish, **kwargs):
697
+ color_print(finish, color="red")
698
+
699
+ def on_tool_start(self, serialized, input_str, **kwargs):
700
+ tool_name = serialized.get("name")
701
+ color_print(f"Calling {tool_name} with inputs: {input_str}", color="pink")
702
+
703
+ def on_tool_end(self, output, **kwargs):
704
+ output = str(output)
705
+ color_print(output, color="blue")
706
+
707
+
708
+ class TokenUsageTracker(BaseCallbackHandler):
709
+ """Collect provider-reported token usage across LangChain LLM calls."""
710
+
711
+ def __init__(self):
712
+ self.reset()
713
+
714
+ def reset(self):
715
+ self.prompt_tokens = 0
716
+ self.completion_tokens = 0
717
+ self.total_tokens = 0
718
+ self.call_count = 0
719
+
720
+ def snapshot(self):
721
+ return {
722
+ "prompt_tokens": int(self.prompt_tokens),
723
+ "completion_tokens": int(self.completion_tokens),
724
+ "total_tokens": int(self.total_tokens),
725
+ "llm_call_count": int(self.call_count),
726
+ }
727
+
728
+ def on_llm_end(self, response, **kwargs):
729
+ usage = self._extract_usage(response)
730
+ if not usage:
731
+ return
732
+ self.prompt_tokens += usage.get("prompt_tokens", 0)
733
+ self.completion_tokens += usage.get("completion_tokens", 0)
734
+ self.total_tokens += usage.get("total_tokens", 0)
735
+ self.call_count += 1
736
+
737
+ def _extract_usage(self, response):
738
+ llm_output = getattr(response, "llm_output", None)
739
+ if isinstance(llm_output, dict):
740
+ usage = self._normalize_usage(llm_output)
741
+ if usage:
742
+ return usage
743
+
744
+ generations = getattr(response, "generations", None) or []
745
+ for generation_group in generations:
746
+ for generation in generation_group:
747
+ message = getattr(generation, "message", None)
748
+ metadata = getattr(message, "response_metadata", None)
749
+ usage = self._normalize_usage(metadata)
750
+ if usage:
751
+ return usage
752
+ usage_meta = getattr(message, "usage_metadata", None)
753
+ usage = self._normalize_usage(usage_meta)
754
+ if usage:
755
+ return usage
756
+ generation_info = getattr(generation, "generation_info", None)
757
+ usage = self._normalize_usage(generation_info)
758
+ if usage:
759
+ return usage
760
+ return None
761
+
762
+ def _normalize_usage(self, payload):
763
+ if not isinstance(payload, dict):
764
+ return None
765
+ nested = None
766
+ for key in ("token_usage", "usage", "usage_metadata"):
767
+ value = payload.get(key)
768
+ if isinstance(value, dict):
769
+ nested = value
770
+ break
771
+ source = nested or payload
772
+
773
+ prompt = self._first_int(
774
+ source,
775
+ "prompt_tokens",
776
+ "input_tokens",
777
+ "prompt_token_count",
778
+ )
779
+ completion = self._first_int(
780
+ source,
781
+ "completion_tokens",
782
+ "output_tokens",
783
+ "completion_token_count",
784
+ )
785
+ total = self._first_int(
786
+ source,
787
+ "total_tokens",
788
+ "total_token_count",
789
+ )
790
+ if total is None and prompt is not None and completion is not None:
791
+ total = prompt + completion
792
+ if prompt is None and completion is None and total is None:
793
+ return None
794
+ return {
795
+ "prompt_tokens": int(prompt or 0),
796
+ "completion_tokens": int(completion or 0),
797
+ "total_tokens": int(total or 0),
798
+ }
799
+
800
+ @staticmethod
801
+ def _first_int(payload, *keys):
802
+ for key in keys:
803
+ value = payload.get(key)
804
+ if isinstance(value, int):
805
+ return value
806
+ if isinstance(value, float):
807
+ return int(value)
808
+ return None
809
+
810
+
811
+ def check_or_create_path(path=None):
812
+ # Set a default path if none is provided
813
+ if path is None:
814
+ path = os.path.join(os.getcwd(), "tmp_directory")
815
+
816
+ # Check if the path exists
817
+ if not os.path.exists(path):
818
+ # If it doesn't exist, create the directory
819
+ os.makedirs(path)
820
+ print(f"Directory created at: {path}")
821
+ else:
822
+ print(f"Directory already exists at: {path}")
823
+
824
+ return path
825
+
826
+
827
+ def langchain_to_gradio_message(message):
828
+ # Build the title and content based on the message type
829
+ if isinstance(message.content, list):
830
+ # For a message with multiple content items (like text and tool use)
831
+ gradio_messages = []
832
+ for item in message.content:
833
+ gradio_message = {
834
+ "role": "user" if message.type == "human" else "assistant",
835
+ "content": "",
836
+ "metadata": {},
837
+ }
838
+
839
+ if item["type"] == "text":
840
+ item["text"] = item["text"].replace("<think>", "\n")
841
+ item["text"] = item["text"].replace("</think>", "\n")
842
+ gradio_message["content"] += f"{item['text']}\n"
843
+ gradio_messages.append(gradio_message)
844
+ elif item["type"] == "tool_use":
845
+ if item["name"] == "run_python_repl":
846
+ gradio_message["metadata"]["title"] = "🛠️ Writing code..."
847
+ # input = "```python {code_block}```\n".format(code_block=item['input']["command"])
848
+ gradio_message["metadata"]["log"] = "Executing Code block..."
849
+ gradio_message["content"] = f"##### Code: \n ```python \n {item['input']['command']} \n``` \n"
850
+ else:
851
+ gradio_message["metadata"]["title"] = f"🛠️ Used tool ```{item['name']}```"
852
+ to_print = ";".join([i + ": " + str(j) for i, j in item["input"].items()])
853
+ gradio_message["metadata"]["log"] = f"🔍 Input -- {to_print}\n"
854
+ gradio_message["metadata"]["status"] = "pending"
855
+ gradio_messages.append(gradio_message)
856
+
857
+ else:
858
+ gradio_message = {
859
+ "role": "user" if message.type == "human" else "assistant",
860
+ "content": "",
861
+ "metadata": {},
862
+ }
863
+ print(message)
864
+ content = message.content
865
+ content = content.replace("<think>", "\n")
866
+ content = content.replace("</think>", "\n")
867
+ content = content.replace("<solution>", "\n")
868
+ content = content.replace("</solution>", "\n")
869
+
870
+ gradio_message["content"] = content
871
+ gradio_messages = [gradio_message]
872
+ return gradio_messages
873
+
874
+
875
+ def parse_hpo_obo(file_path):
876
+ """Parse the HPO OBO file and create a dictionary mapping HP IDs to phenotype descriptions.
877
+
878
+ Args:
879
+ file_path (str): Path to the HPO OBO file.
880
+
881
+ Returns:
882
+ dict: A dictionary where keys are HP IDs and values are phenotype descriptions.
883
+
884
+ """
885
+ hp_dict = {}
886
+ current_id = None
887
+ current_name = None
888
+
889
+ with open(file_path) as file:
890
+ for line in file:
891
+ line = line.strip()
892
+ if line.startswith("[Term]"):
893
+ # If a new term block starts, save the previous term
894
+ if current_id and current_name:
895
+ hp_dict[current_id] = current_name
896
+ current_id = None
897
+ current_name = None
898
+ elif line.startswith("id: HP:"):
899
+ current_id = line.split(": ")[1]
900
+ elif line.startswith("name:"):
901
+ current_name = line.split(": ", 1)[1]
902
+
903
+ # Add the last term to the dictionary
904
+ if current_id and current_name:
905
+ hp_dict[current_id] = current_name
906
+
907
+ return hp_dict
908
+
909
+
910
+ def textify_api_dict(api_dict):
911
+ """Convert a nested API dictionary to a nicely formatted string."""
912
+ lines = []
913
+ for category, methods in api_dict.items():
914
+ lines.append(f"Import file: {category}")
915
+ lines.append("=" * (len("Import file: ") + len(category)))
916
+ for method in methods:
917
+ lines.append(f"Method: {method.get('name', 'N/A')}")
918
+ lines.append(f" Description: {method.get('description', 'No description provided.')}")
919
+
920
+ # Process required parameters
921
+ req_params = method.get("required_parameters", [])
922
+ if req_params:
923
+ lines.append(" Required Parameters:")
924
+ for param in req_params:
925
+ param_name = param.get("name", "N/A")
926
+ param_type = param.get("type", "N/A")
927
+ param_desc = param.get("description", "No description")
928
+ param_default = param.get("default", "None")
929
+ lines.append(f" - {param_name} ({param_type}): {param_desc} [Default: {param_default}]")
930
+
931
+ # Process optional parameters
932
+ opt_params = method.get("optional_parameters", [])
933
+ if opt_params:
934
+ lines.append(" Optional Parameters:")
935
+ for param in opt_params:
936
+ param_name = param.get("name", "N/A")
937
+ param_type = param.get("type", "N/A")
938
+ param_desc = param.get("description", "No description")
939
+ param_default = param.get("default", "None")
940
+ lines.append(f" - {param_name} ({param_type}): {param_desc} [Default: {param_default}]")
941
+
942
+ lines.append("") # Empty line between methods
943
+ lines.append("") # Extra empty line after each category
944
+
945
+ return "\n".join(lines)
946
+
947
+
948
+ def read_module2api():
949
+ fields = [
950
+ "literature",
951
+ "biochemistry",
952
+ "bioimaging",
953
+ "bioengineering",
954
+ "biophysics",
955
+ "glycoengineering",
956
+ "cancer_biology",
957
+ "cell_biology",
958
+ "molecular_biology",
959
+ "genetics",
960
+ "genomics",
961
+ "immunology",
962
+ "microbiology",
963
+ "pathology",
964
+ "pharmacology",
965
+ "physiology",
966
+ "synthetic_biology",
967
+ "systems_biology",
968
+ "support_tools",
969
+ "database",
970
+ "lab_automation",
971
+ "protocols",
972
+ ]
973
+
974
+ module2api = {}
975
+ for field in fields:
976
+ module_name = f"biomni.tool.tool_description.{field}"
977
+ module = importlib.import_module(module_name)
978
+ module2api[f"biomni.tool.{field}"] = module.description
979
+ return module2api
980
+
981
+
982
+ def download_and_unzip(url: str, dest_dir: str) -> str:
983
+ """Download a zip file from a URL and extract it to the destination directory.
984
+
985
+ Args:
986
+ url: The URL to download the zip file from.
987
+ dest_dir: The directory to extract the contents to.
988
+
989
+ Returns:
990
+ The path to the extracted directory, or an error message.
991
+
992
+ """
993
+ try:
994
+ os.makedirs(dest_dir, exist_ok=True)
995
+ print(f"Downloading from {url} ...")
996
+ with requests.get(url, stream=True) as r:
997
+ r.raise_for_status()
998
+ total_size = int(r.headers.get("content-length", 0))
999
+ chunk_size = 8192
1000
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file:
1001
+ with tqdm.tqdm(
1002
+ total=total_size / (1024**3),
1003
+ unit="GB",
1004
+ unit_scale=True,
1005
+ desc="Downloading",
1006
+ ncols=80,
1007
+ ) as pbar:
1008
+ for chunk in r.iter_content(chunk_size=chunk_size):
1009
+ if chunk:
1010
+ tmp_file.write(chunk)
1011
+ pbar.update(len(chunk) / (1024**3))
1012
+ tmp_zip_path = tmp_file.name
1013
+ print(f"Downloaded to {tmp_zip_path}. Extracting...")
1014
+ with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref:
1015
+ zip_ref.extractall(dest_dir)
1016
+ os.unlink(tmp_zip_path)
1017
+ print(f"Extraction complete to {dest_dir}")
1018
+ return dest_dir
1019
+ except Exception as e:
1020
+ print(f"Error downloading or extracting zip: {e}")
1021
+ return f"Error: {e}"
1022
+
1023
+
1024
+ def check_and_download_s3_files(
1025
+ s3_bucket_url: str, local_data_lake_path: str, expected_files: list[str], folder: str = "data_lake"
1026
+ ) -> dict[str, bool]:
1027
+ """Check for missing files in the local data lake and download them from S3 bucket.
1028
+
1029
+ Args:
1030
+ s3_bucket_url: Base URL of the S3 bucket (e.g., "https://biomni-release.s3.amazonaws.com")
1031
+ local_data_lake_path: Local path to the data lake directory
1032
+ expected_files: List of expected file names in the data lake
1033
+ folder: S3 folder name ("data_lake" or "benchmark")
1034
+
1035
+ Returns:
1036
+ Dictionary mapping file names to download success status
1037
+ """
1038
+
1039
+ os.makedirs(local_data_lake_path, exist_ok=True)
1040
+ download_results = {}
1041
+
1042
+ def download_with_progress(url: str, file_path: str, desc: str) -> bool:
1043
+ """Download file with progress bar."""
1044
+ try:
1045
+ response = requests.get(url, stream=True)
1046
+ response.raise_for_status()
1047
+
1048
+ total_size = int(response.headers.get("content-length", 0))
1049
+
1050
+ with open(file_path, "wb") as f:
1051
+ if total_size > 0:
1052
+ with tqdm.tqdm(total=total_size, unit="B", unit_scale=True, desc=desc, ncols=80) as pbar:
1053
+ for chunk in response.iter_content(chunk_size=8192):
1054
+ if chunk:
1055
+ f.write(chunk)
1056
+ pbar.update(len(chunk))
1057
+ else:
1058
+ for chunk in response.iter_content(chunk_size=8192):
1059
+ if chunk:
1060
+ f.write(chunk)
1061
+ return True
1062
+ except Exception as e:
1063
+ print(f"✗ Failed to download {desc}: {e}")
1064
+ if os.path.exists(file_path):
1065
+ try:
1066
+ os.remove(file_path)
1067
+ except OSError:
1068
+ pass
1069
+ return False
1070
+
1071
+ def cleanup_file(file_path: str):
1072
+ """Clean up file if it exists."""
1073
+ if os.path.exists(file_path):
1074
+ try:
1075
+ os.remove(file_path)
1076
+ except OSError:
1077
+ pass
1078
+
1079
+ # Handle benchmark folder (download as zip)
1080
+ if folder == "benchmark":
1081
+ print(f"Downloading entire {folder} folder structure...")
1082
+ s3_zip_url = urljoin(s3_bucket_url + "/", folder + ".zip")
1083
+
1084
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
1085
+ tmp_zip_path = tmp_zip.name
1086
+
1087
+ if download_with_progress(s3_zip_url, tmp_zip_path, f"{folder}.zip"):
1088
+ print(f"Extracting {folder}.zip...")
1089
+ try:
1090
+ with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref:
1091
+ zip_ref.extractall(local_data_lake_path)
1092
+ print(f"✓ Successfully downloaded and extracted {folder} folder")
1093
+ download_results = dict.fromkeys(expected_files, True)
1094
+ except Exception as e:
1095
+ print(f"✗ Error extracting {folder}.zip: {e}")
1096
+ download_results = dict.fromkeys(expected_files, False)
1097
+ finally:
1098
+ cleanup_file(tmp_zip_path)
1099
+ else:
1100
+ download_results = dict.fromkeys(expected_files, False)
1101
+
1102
+ return download_results
1103
+
1104
+ # Handle data_lake folder (download individual files)
1105
+ for filename in expected_files:
1106
+ local_file_path = os.path.join(local_data_lake_path, filename)
1107
+
1108
+ if os.path.exists(local_file_path):
1109
+ download_results[filename] = True
1110
+ continue
1111
+
1112
+ s3_file_url = urljoin(s3_bucket_url + "/" + folder + "/", filename)
1113
+ print(f"Downloading {filename} from {folder}...")
1114
+
1115
+ if download_with_progress(s3_file_url, local_file_path, filename):
1116
+ print(f"✓ Successfully downloaded: {filename}")
1117
+ download_results[filename] = True
1118
+ else:
1119
+ download_results[filename] = False
1120
+
1121
+ return download_results
1122
+
1123
+
1124
+ def clean_message_content(content: str) -> str:
1125
+ """Clean message content by removing ANSI escape codes.
1126
+
1127
+ This function removes ANSI escape sequences (like color codes) from text content
1128
+ that might be present in terminal output or console messages. This ensures clean
1129
+ text for markdown generation and PDF conversion.
1130
+
1131
+ Args:
1132
+ content: The raw message content that may contain ANSI escape codes
1133
+
1134
+ Returns:
1135
+ Cleaned content with ANSI escape codes removed
1136
+
1137
+ Example:
1138
+ >>> clean_message_content("Hello \x1b[31mworld\x1b[0m!")
1139
+ "Hello world!"
1140
+ """
1141
+ import re
1142
+
1143
+ return re.sub(r"\x1b\[[0-9;]*m", "", content)
1144
+
1145
+
1146
+ def should_skip_message(clean_output: str) -> bool:
1147
+ """Check if message should be skipped during markdown generation.
1148
+
1149
+ This function determines whether a message should be excluded from the final
1150
+ markdown output. It skips empty or meaningless messages but preserves important
1151
+ error messages that should be displayed to users.
1152
+
1153
+ Args:
1154
+ clean_output: The cleaned message content to evaluate
1155
+
1156
+ Returns:
1157
+ True if the message should be skipped, False otherwise
1158
+
1159
+ Note:
1160
+ Parsing error messages are intentionally not skipped as they provide
1161
+ important feedback to users about conversation flow issues.
1162
+ """
1163
+ return (
1164
+ clean_output.strip() in ["", "None", "null", "undefined"]
1165
+ # Don't skip parsing error messages - they should be displayed and increment step counter
1166
+ # or "There are no tags" in clean_output
1167
+ # or "Execution terminated due to repeated parsing errors" in clean_output
1168
+ )
1169
+
1170
+
1171
+ def has_execution_results(clean_output: str, execution_results) -> bool:
1172
+ """Check if message contains code execution and has associated results.
1173
+
1174
+ This function determines whether a message contains executable code and has
1175
+ corresponding execution results available for display in the markdown output.
1176
+
1177
+ Args:
1178
+ clean_output: The cleaned message content to check for execute tags
1179
+ execution_results: List of execution results from the agent's execution history
1180
+
1181
+ Returns:
1182
+ True if the message contains <execute> tags and has execution results available
1183
+ """
1184
+ return "<execute>" in clean_output and execution_results is not None and execution_results
1185
+
1186
+
1187
+ def find_matching_execution(clean_output: str, execution_results) -> dict | None:
1188
+ """Find the execution result that matches the given message content.
1189
+
1190
+ This function searches through the execution results to find the one that
1191
+ corresponds to the current message. It matches based on the triggering message
1192
+ content to associate execution results with their originating AI messages.
1193
+
1194
+ Args:
1195
+ clean_output: The cleaned message content to match against
1196
+ execution_results: List of execution result dictionaries containing
1197
+ triggering messages and execution data
1198
+
1199
+ Returns:
1200
+ The matching execution result dictionary if found, None otherwise
1201
+
1202
+ Note:
1203
+ The matching is bidirectional - it checks if either the triggering message
1204
+ is contained in the current output or vice versa to handle partial matches.
1205
+ """
1206
+ for exec_result in execution_results:
1207
+ if exec_result["triggering_message"] in clean_output or clean_output in exec_result["triggering_message"]:
1208
+ return exec_result
1209
+ return None
1210
+
1211
+
1212
+ def create_parsing_error_html() -> str:
1213
+ """Create HTML markup for displaying parsing errors in markdown output.
1214
+
1215
+ This function generates a styled HTML block that displays parsing errors
1216
+ when the agent's response doesn't contain the required tags. The HTML
1217
+ uses CSS classes for consistent styling in the final PDF output.
1218
+
1219
+ Returns:
1220
+ HTML string containing a styled parsing error message box
1221
+
1222
+ Note:
1223
+ The returned HTML uses CSS classes defined in get_pdf_css_content()
1224
+ for consistent styling across the document.
1225
+ """
1226
+ return """
1227
+ <div class="parsing-error-box">
1228
+ <div class="parsing-error-header">Parsing Error</div>
1229
+ <div class="parsing-error-content">Each response must include thinking process followed by either execute or solution tag. But there are no tags in the current response.</div>
1230
+ </div>
1231
+ """
1232
+
1233
+
1234
+ def parse_tool_calls_from_code(code: str, module2api: dict, custom_functions: dict = None) -> list[str]:
1235
+ """Parse code to detect imported tools by analyzing import statements.
1236
+
1237
+ This function analyzes Python code to identify which tools/functions are being
1238
+ imported and used. It extracts tool names from import statements and function
1239
+ calls, then returns a deduplicated list of detected tool names.
1240
+
1241
+ Args:
1242
+ code: The Python code string to analyze for tool imports
1243
+ module2api: Dictionary mapping module names to their available API tools
1244
+ custom_functions: Optional dictionary of custom functions that have been
1245
+ added to the agent
1246
+
1247
+ Returns:
1248
+ Sorted list of unique tool names detected in the code
1249
+
1250
+ Example:
1251
+ >>> code = "from biomni.tool import analyze_data\nimport pandas as pd"
1252
+ >>> parse_tool_calls_from_code(code, module2api)
1253
+ ['analyze_data', 'pandas']
1254
+ """
1255
+ tool_module_pairs = parse_tool_calls_with_modules(code, module2api, custom_functions)
1256
+ return sorted({pair[0] for pair in tool_module_pairs})
1257
+
1258
+
1259
+ def parse_tool_calls_with_modules(code: str, module2api: dict, custom_functions: dict = None) -> list[tuple[str, str]]:
1260
+ """Parse code to detect imported tools and their associated modules.
1261
+
1262
+ This function performs detailed analysis of Python code to identify which
1263
+ tools/functions are being imported and which modules they belong to. It
1264
+ handles various import patterns including direct imports, from-imports,
1265
+ and module.function patterns.
1266
+
1267
+ Args:
1268
+ code: The Python code string to analyze for tool imports
1269
+ module2api: Dictionary mapping module names to their available API tools
1270
+ custom_functions: Optional dictionary of custom functions that have been
1271
+ added to the agent
1272
+
1273
+ Returns:
1274
+ List of tuples containing (tool_name, module_name) pairs for each
1275
+ detected tool and its associated module
1276
+
1277
+ Note:
1278
+ The function uses regex patterns to match various import statement
1279
+ formats and also detects direct function calls without explicit imports.
1280
+ """
1281
+ import re
1282
+
1283
+ detected_tools = set()
1284
+
1285
+ # Get all available tools from module2api
1286
+ all_tools = {}
1287
+ for module_name, module_tools in module2api.items():
1288
+ for tool in module_tools:
1289
+ if isinstance(tool, dict) and "name" in tool:
1290
+ tool_name = tool["name"]
1291
+ if tool_name not in all_tools:
1292
+ all_tools[tool_name] = []
1293
+ all_tools[tool_name].append(module_name)
1294
+
1295
+ # Add custom tools
1296
+ if custom_functions:
1297
+ for tool_name in custom_functions.keys():
1298
+ if tool_name not in all_tools:
1299
+ all_tools[tool_name] = []
1300
+ all_tools[tool_name].append("custom_tools")
1301
+
1302
+ # Look for import statements in the code
1303
+ import_patterns = [
1304
+ r"from\s+([\w.]+)\s+import\s+([\w,\s]+)", # from module import tool1, tool2
1305
+ r"import\s+([\w.]+)", # import module
1306
+ ]
1307
+
1308
+ for pattern in import_patterns:
1309
+ matches = re.findall(pattern, code)
1310
+ for match in matches:
1311
+ if len(match) == 2: # from module import tools
1312
+ module_name, tools_str = match
1313
+ # Split tools by comma and clean up
1314
+ tools = [tool.strip() for tool in tools_str.split(",")]
1315
+
1316
+ for tool in tools:
1317
+ # Check if this tool exists in any module
1318
+ if tool in all_tools:
1319
+ # Find the best matching module
1320
+ best_module = find_best_module_match(module_name, all_tools[tool])
1321
+ detected_tools.add((tool, best_module))
1322
+ # Also check if it's a module.function pattern
1323
+ elif "." in tool:
1324
+ parts = tool.split(".")
1325
+ if len(parts) == 2:
1326
+ module_part, func_part = parts
1327
+ if func_part in all_tools:
1328
+ best_module = find_best_module_match(module_part, all_tools[func_part])
1329
+ detected_tools.add((func_part, best_module))
1330
+
1331
+ elif len(match) == 1: # import module
1332
+ module_name = match[0]
1333
+ # Check if any tools from this module are used
1334
+ for tool_name, modules in all_tools.items():
1335
+ if any(module_name in mod for mod in modules):
1336
+ # Look for usage of this tool in the code
1337
+ if re.search(rf"\b{tool_name}\s*\(", code):
1338
+ best_module = find_best_module_match(module_name, modules)
1339
+ detected_tools.add((tool_name, best_module))
1340
+
1341
+ # Also look for direct function calls without imports
1342
+ function_call_pattern = r"(\w+)\s*\("
1343
+ function_calls = re.findall(function_call_pattern, code)
1344
+
1345
+ for func_call in function_calls:
1346
+ if func_call in all_tools:
1347
+ # For direct calls, use the first available module
1348
+ best_module = all_tools[func_call][0]
1349
+ detected_tools.add((func_call, best_module))
1350
+
1351
+ return sorted(detected_tools)
1352
+
1353
+
1354
+ def find_best_module_match(target_module: str, available_modules: list[str]) -> str:
1355
+ """Find the best matching module from a list of available modules.
1356
+
1357
+ This function attempts to match a target module name against a list of
1358
+ available modules using various matching strategies: exact match, partial
1359
+ substring matches, and fallback to the first available module.
1360
+
1361
+ Args:
1362
+ target_module: The module name we're trying to match
1363
+ available_modules: List of available module names to search through
1364
+
1365
+ Returns:
1366
+ The best matching module name from the available modules list.
1367
+ Returns "unknown" if no modules are available.
1368
+
1369
+ Note:
1370
+ The matching strategy prioritizes exact matches, then partial matches
1371
+ (where either the target is contained in the module name or vice versa),
1372
+ and finally falls back to the first available module.
1373
+ """
1374
+ # First try exact match
1375
+ if target_module in available_modules:
1376
+ return target_module
1377
+
1378
+ # Try partial matches
1379
+ for module in available_modules:
1380
+ if target_module in module or module in target_module:
1381
+ return module
1382
+
1383
+ # Return the first available module as fallback
1384
+ return available_modules[0] if available_modules else "unknown"
1385
+
1386
+
1387
+ def inject_custom_functions_to_repl(custom_functions: dict):
1388
+ """Inject custom functions into the Python REPL execution environment.
1389
+
1390
+ This function makes custom tools available during code execution by injecting
1391
+ them into both the persistent execution namespace and the builtins module.
1392
+ This allows the agent to call custom functions that users have added via
1393
+ agent.add_tool() when executing Python code in <execute> blocks.
1394
+
1395
+ Args:
1396
+ custom_functions: Dictionary mapping function names to their callable objects
1397
+
1398
+ Note:
1399
+ The function modifies both the persistent namespace used by run_python_repl
1400
+ and the builtins module to ensure maximum compatibility and accessibility
1401
+ of custom functions during code execution.
1402
+ """
1403
+ if custom_functions:
1404
+ # Access the persistent namespace used by run_python_repl
1405
+ from biomni.tool.support_tools import _persistent_namespace
1406
+
1407
+ # Inject all custom functions into the execution namespace
1408
+ for name, func in custom_functions.items():
1409
+ _persistent_namespace[name] = func
1410
+
1411
+ # Also make them available in builtins for broader access
1412
+ import builtins
1413
+
1414
+ if not hasattr(builtins, "_biomni_custom_functions"):
1415
+ builtins._biomni_custom_functions = {}
1416
+ builtins._biomni_custom_functions.update(custom_functions)
1417
+
1418
+
1419
+ def format_execute_tags_in_content(content: str, parse_tool_calls_with_modules_func) -> str:
1420
+ """Format execute tags in content by extracting code and creating highlighted tool call blocks.
1421
+
1422
+ This function processes content that contains <execute>...</execute> tags and
1423
+ converts them into styled HTML blocks that display the code with syntax highlighting
1424
+ and information about which tools are being used.
1425
+
1426
+ Args:
1427
+ content: The content string that may contain <execute> tags
1428
+ parse_tool_calls_with_modules_func: Function to parse tool calls with modules
1429
+ (typically parse_tool_calls_with_modules)
1430
+
1431
+ Returns:
1432
+ Formatted content with execute tags converted to highlighted tool call blocks.
1433
+ Also processes <solution> tags in the same pass.
1434
+
1435
+ Note:
1436
+ The function also calls format_solution_tags_in_content() to handle
1437
+ solution tags in the same processing pass.
1438
+ """
1439
+ import re
1440
+
1441
+ # Pattern to match <execute>...</execute> blocks
1442
+ execute_pattern = r"<execute>(.*?)</execute>"
1443
+
1444
+ def replace_execute_tag(match):
1445
+ code_content = match.group(1).strip()
1446
+ language, tool_name = detect_code_language_and_tool(code_content)
1447
+ code_content = clean_code_content(code_content, language)
1448
+
1449
+ # Parse tools from the code content with module information
1450
+ detected_tool_modules = parse_tool_calls_with_modules_func(code_content)
1451
+
1452
+ # Create the formatted block
1453
+ formatted_block = create_tool_call_block(code_content, language, tool_name, detected_tool_modules)
1454
+ return formatted_block
1455
+
1456
+ # Replace all execute tags with formatted tool call blocks
1457
+ formatted_content = re.sub(execute_pattern, replace_execute_tag, content, flags=re.DOTALL)
1458
+
1459
+ # Also format solution tags
1460
+ formatted_content = format_solution_tags_in_content(formatted_content)
1461
+
1462
+ return formatted_content
1463
+
1464
+
1465
+ def detect_code_language_and_tool(code_content: str) -> tuple[str, str]:
1466
+ """Detect the programming language and tool name from code content.
1467
+
1468
+ This function analyzes code content to determine the programming language
1469
+ and appropriate tool name based on language markers at the beginning of
1470
+ the code block.
1471
+
1472
+ Args:
1473
+ code_content: The code content to analyze for language markers
1474
+
1475
+ Returns:
1476
+ Tuple containing (language, tool_name) where:
1477
+ - language: The detected programming language ("python", "r", "bash")
1478
+ - tool_name: The human-readable tool name for display
1479
+
1480
+ Example:
1481
+ >>> detect_code_language_and_tool("#!R\nlibrary(ggplot2)")
1482
+ ("r", "R REPL")
1483
+ >>> detect_code_language_and_tool("#!BASH\necho 'hello'")
1484
+ ("bash", "Bash Script")
1485
+ """
1486
+ if code_content.startswith("#!R") or code_content.startswith("# R code") or code_content.startswith("# R script"):
1487
+ return "r", "R REPL"
1488
+ elif code_content.startswith("#!BASH") or code_content.startswith("# Bash script"):
1489
+ return "bash", "Bash Script"
1490
+ elif code_content.startswith("#!CLI"):
1491
+ return "bash", "CLI Command"
1492
+ else:
1493
+ return "python", "Python REPL"
1494
+
1495
+
1496
+ def clean_code_content(code_content: str, language: str) -> str:
1497
+ """Clean code content by removing language markers.
1498
+
1499
+ This function removes language-specific markers from the beginning of code
1500
+ content to prepare it for display in code blocks. The markers are used
1501
+ internally for language detection but should not appear in the final output.
1502
+
1503
+ Args:
1504
+ code_content: The raw code content that may contain language markers
1505
+ language: The detected programming language ("python", "r", "bash")
1506
+
1507
+ Returns:
1508
+ Cleaned code content with language markers removed
1509
+
1510
+ Example:
1511
+ >>> clean_code_content("#!R\nlibrary(ggplot2)", "r")
1512
+ "library(ggplot2)"
1513
+ >>> clean_code_content("#!BASH\necho 'hello'", "bash")
1514
+ "echo 'hello'"
1515
+ """
1516
+ import re
1517
+
1518
+ if language == "r":
1519
+ return re.sub(r"^#!R|^# R code|^# R script", "", code_content, count=1).strip()
1520
+ elif language == "bash":
1521
+ if code_content.startswith("#!BASH") or code_content.startswith("# Bash script"):
1522
+ return re.sub(r"^#!BASH|^# Bash script", "", code_content, count=1).strip()
1523
+ elif code_content.startswith("#!CLI"):
1524
+ return re.sub(r"^#!CLI", "", code_content, count=1).strip()
1525
+ return code_content
1526
+
1527
+
1528
+ def create_tool_call_block(code_content: str, language: str, tool_name: str, detected_tool_modules: list) -> str:
1529
+ """Create the HTML block for tool call highlighting.
1530
+
1531
+ This function generates a styled HTML block that displays code execution
1532
+ information including the code itself, syntax highlighting, and a list of
1533
+ tools that were used during execution.
1534
+
1535
+ Args:
1536
+ code_content: The cleaned code content to display
1537
+ language: The programming language for syntax highlighting
1538
+ tool_name: The default tool name to display if no specific tools detected
1539
+ detected_tool_modules: List of (tool_name, module_name) tuples for tools used
1540
+
1541
+ Returns:
1542
+ HTML string containing a styled tool call block with code and tool information
1543
+
1544
+ Note:
1545
+ The HTML uses CSS classes defined in get_pdf_css_content() for styling.
1546
+ If no specific tools are detected, it falls back to a default tool name.
1547
+ """
1548
+ # Create the formatted block with code and tools used
1549
+ formatted_block = f"""<div class="tool-call-highlight">
1550
+ <div class="tool-call-header">
1551
+ <strong>Code Execution</strong>
1552
+ </div>
1553
+ <div class="tool-call-input">
1554
+ ```{language}
1555
+ {code_content}
1556
+ ```
1557
+ </div>"""
1558
+
1559
+ # Add tools used section
1560
+ if detected_tool_modules:
1561
+ tools_list = format_detected_tools(detected_tool_modules)
1562
+ formatted_block += f"""
1563
+ <div class="tools-used">
1564
+ <strong>Tools Used:</strong> {tools_list}
1565
+ </div>"""
1566
+ else:
1567
+ formatted_block += format_default_tool_name(language, tool_name)
1568
+
1569
+ formatted_block += "</div>"
1570
+ return formatted_block
1571
+
1572
+
1573
+ def format_detected_tools(detected_tool_modules: list) -> str:
1574
+ """Format detected tools with their modules for display.
1575
+
1576
+ This function takes a list of (tool_name, module_name) tuples and formats
1577
+ them into a human-readable string for display in the tool call blocks.
1578
+ It handles special cases for common tools and formats module names appropriately.
1579
+
1580
+ Args:
1581
+ detected_tool_modules: List of (tool_name, module_name) tuples
1582
+
1583
+ Returns:
1584
+ Comma-separated string of formatted tool descriptions
1585
+
1586
+ Example:
1587
+ >>> format_detected_tools([("analyze_data", "biomni.tool"), ("pandas", "pandas")])
1588
+ "biomni → analyze_data, pandas → pandas"
1589
+ """
1590
+ tool_descriptions = []
1591
+ for tool_name, module_name in detected_tool_modules:
1592
+ if tool_name == "python_repl":
1593
+ tool_descriptions.append("Python REPL")
1594
+ elif tool_name == "r_repl":
1595
+ tool_descriptions.append("R REPL")
1596
+ elif "bash" in tool_name.lower():
1597
+ tool_descriptions.append("Bash Script")
1598
+ else:
1599
+ # Extract the last part of the module name for display
1600
+ display_module = module_name.split(".")[-1] if "." in module_name else module_name
1601
+ tool_descriptions.append(f"{display_module} → {tool_name}")
1602
+
1603
+ return ", ".join(sorted(tool_descriptions))
1604
+
1605
+
1606
+ def format_default_tool_name(language: str, tool_name: str) -> str:
1607
+ """Format default tool name based on programming language.
1608
+
1609
+ This function generates HTML for displaying the default tool name when
1610
+ no specific tools are detected in the code. It maps programming languages
1611
+ to their appropriate default tool names.
1612
+
1613
+ Args:
1614
+ language: The programming language ("python", "r", "bash")
1615
+ tool_name: The detected tool name (used for bash CLI vs script distinction)
1616
+
1617
+ Returns:
1618
+ HTML string containing a styled tools-used section
1619
+
1620
+ Note:
1621
+ For bash, it distinguishes between CLI commands and bash scripts
1622
+ based on the tool_name parameter.
1623
+ """
1624
+ if language == "r":
1625
+ return """
1626
+ <div class="tools-used">
1627
+ <strong>Tools Used:</strong> R REPL
1628
+ </div>"""
1629
+ elif language == "bash":
1630
+ if tool_name == "CLI Command":
1631
+ return """
1632
+ <div class="tools-used">
1633
+ <strong>Tools Used:</strong> CLI Command
1634
+ </div>"""
1635
+ else:
1636
+ return """
1637
+ <div class="tools-used">
1638
+ <strong>Tools Used:</strong> Bash Script
1639
+ </div>"""
1640
+ else:
1641
+ return """
1642
+ <div class="tools-used">
1643
+ <strong>Tools Used:</strong> Python REPL
1644
+ </div>"""
1645
+
1646
+
1647
+ def format_solution_tags_in_content(content: str) -> str:
1648
+ """Format solution tags in content by extracting text and formatting as solution blocks.
1649
+
1650
+ This function processes content that contains <solution>...</solution> tags and
1651
+ converts them into styled HTML blocks that display solution content with appropriate
1652
+ formatting and CSS classes.
1653
+
1654
+ Args:
1655
+ content: The content string that may contain <solution> tags
1656
+
1657
+ Returns:
1658
+ Formatted content with solution tags converted to styled solution blocks
1659
+
1660
+ Note:
1661
+ The solution blocks use the "title-text summary" CSS class for consistent
1662
+ styling with other content blocks in the markdown output.
1663
+ """
1664
+ import re
1665
+
1666
+ # Pattern to match <solution>...</solution> blocks
1667
+ solution_pattern = r"<solution>(.*?)</solution>"
1668
+
1669
+ def replace_solution_tag(match):
1670
+ solution_content = match.group(1).strip()
1671
+ # Format as regular text, not terminal
1672
+ return f"""<div class="title-text summary">
1673
+ <div class="title-text-header">
1674
+ <strong>Summary and Solution</strong>
1675
+ </div>
1676
+ <div class="title-text-content">
1677
+ {solution_content}
1678
+ </div>
1679
+ </div>"""
1680
+
1681
+ # Replace all solution tags with formatted solution blocks
1682
+ formatted_content = re.sub(solution_pattern, replace_solution_tag, content, flags=re.DOTALL)
1683
+
1684
+ return formatted_content
1685
+
1686
+
1687
+ def format_observation_as_terminal(content: str) -> str | None:
1688
+ """Format observation content with terminal-like styling.
1689
+
1690
+ This function processes observation content from the agent's execution results
1691
+ and formats it as a styled terminal block. It handles both text and image content,
1692
+ with length limits to ensure the output fits within PDF page constraints.
1693
+
1694
+ Args:
1695
+ content: The observation content string, potentially containing <observation> tags
1696
+
1697
+ Returns:
1698
+ Formatted HTML content with terminal styling, or None if observation is
1699
+ empty, invalid, or contains only meaningless content
1700
+
1701
+ Note:
1702
+ - Content is limited to 10,000 characters to fit within 2 A4 pages
1703
+ - Handles both text and base64-encoded images
1704
+ - Uses CSS classes for consistent styling with other content blocks
1705
+ """
1706
+ import re
1707
+
1708
+ # Character limit for 2 A4 pages (approximately 10,000 characters)
1709
+ MAX_OBSERVATION_LENGTH = 10000
1710
+
1711
+ # Remove the <observation> tags and extract the content
1712
+ observation_pattern = r"<observation>(.*?)</observation>"
1713
+ observation_match = re.search(observation_pattern, content, re.DOTALL)
1714
+
1715
+ if observation_match:
1716
+ observation_content = observation_match.group(1).strip()
1717
+ else:
1718
+ # Fallback if no observation tags found - check if content is meaningful
1719
+ if not (content.strip() and content.strip() not in ["", "None", "null", "undefined"]):
1720
+ return None
1721
+ observation_content = content.strip()
1722
+
1723
+ # Skip empty observations
1724
+ if not observation_content or observation_content in ["", "None", "null", "undefined"]:
1725
+ return None
1726
+
1727
+ # Check if observation is too long for 2 pages
1728
+ if len(observation_content) > MAX_OBSERVATION_LENGTH:
1729
+ cropped_content = observation_content[:MAX_OBSERVATION_LENGTH]
1730
+ truncation_notice = f"\n\n[Output truncated - content was too long to display here ({len(observation_content)} characters total)]"
1731
+ observation_content = cropped_content + truncation_notice
1732
+
1733
+ # Check if it contains plot data (base64 images)
1734
+ if "data:image/" in observation_content:
1735
+ content_html = process_observation_with_images(observation_content)
1736
+ else:
1737
+ # Regular text output - format as terminal output
1738
+ content_html = f"```terminal\n{observation_content}\n```"
1739
+
1740
+ return f"""<div class="title-text observation">
1741
+ <div class="title-text-header">
1742
+ <strong>Observation</strong>
1743
+ </div>
1744
+ <div class="title-text-content">
1745
+ {content_html}
1746
+ </div>
1747
+ </div>"""
1748
+
1749
+
1750
+ def process_observation_with_images(observation_content: str) -> str:
1751
+ """Process observation content that contains both text and base64-encoded images.
1752
+
1753
+ This function handles observation content that includes both text output and
1754
+ base64-encoded images (typically plots from data analysis). It separates the
1755
+ text and image content and formats them appropriately for markdown display.
1756
+
1757
+ Args:
1758
+ observation_content: The observation content containing both text and images
1759
+
1760
+ Returns:
1761
+ HTML string containing formatted text (as terminal blocks) and images
1762
+ (as markdown image tags)
1763
+
1764
+ Note:
1765
+ The function uses "data:image/" as a delimiter to split content into
1766
+ text and image parts, then processes each part separately.
1767
+ """
1768
+ # Split content into text and image parts
1769
+ parts = observation_content.split("data:image/")
1770
+ text_parts = []
1771
+ image_parts = []
1772
+
1773
+ for i, part in enumerate(parts):
1774
+ if i == 0:
1775
+ # First part is text only
1776
+ if part.strip():
1777
+ text_parts.append(part.strip())
1778
+ else:
1779
+ # Find the end of the base64 data
1780
+ end_markers = ["\n", "\r", " ", "\t", ">", "<", "]", ")", "}"]
1781
+ image_end = len(part)
1782
+ for marker in end_markers:
1783
+ marker_pos = part.find(marker)
1784
+ if marker_pos != -1 and marker_pos < image_end:
1785
+ image_end = marker_pos
1786
+
1787
+ # Extract image data
1788
+ image_data = "data:image/" + part[:image_end]
1789
+ image_parts.append(image_data)
1790
+
1791
+ # Extract remaining text
1792
+ remaining_text = part[image_end:].strip()
1793
+ if remaining_text:
1794
+ text_parts.append(remaining_text)
1795
+
1796
+ # Build the content
1797
+ content_html = ""
1798
+ if text_parts:
1799
+ # Add text content as terminal output
1800
+ text_content = "\n".join(text_parts)
1801
+ content_html += f"```terminal\n{text_content}\n```\n\n"
1802
+
1803
+ if image_parts:
1804
+ # Add image content
1805
+ for image_data in image_parts:
1806
+ content_html += f"![Plot]({image_data})\n\n"
1807
+
1808
+ return content_html
1809
+
1810
+
1811
+ def remove_emojis_from_text(text: str) -> str:
1812
+ """Remove emojis from text for markdown/PDF output.
1813
+
1814
+ This function removes common emojis used in the system prompt and configuration
1815
+ display from text content before it's converted to markdown or PDF. This ensures
1816
+ clean, professional output while preserving emojis in the console display.
1817
+
1818
+ Args:
1819
+ text: The text content that may contain emojis
1820
+
1821
+ Returns:
1822
+ Text content with emojis removed
1823
+
1824
+ Note:
1825
+ The function targets specific emojis used in the Biomni system:
1826
+ - 🔧 for tools
1827
+ - 📊 for data
1828
+ - ⚙️ for software
1829
+ - 📋 for configuration
1830
+ - 🤖 for agent
1831
+ """
1832
+ import re
1833
+
1834
+ # Remove common emojis used in the system prompt, this makes conversion simpler
1835
+ emoji_patterns = [
1836
+ r"🔧\s*", # Tool emoji
1837
+ r"📊\s*", # Data emoji
1838
+ r"⚙️\s*", # Software emoji
1839
+ r"📋\s*", # Config emoji
1840
+ r"🤖\s*", # Agent emoji
1841
+ ]
1842
+
1843
+ for pattern in emoji_patterns:
1844
+ text = re.sub(pattern, "", text)
1845
+
1846
+ return text
1847
+
1848
+
1849
+ def format_lists_in_text(text: str) -> str:
1850
+ """Format numbered lists and bullet points in text to proper markdown format.
1851
+
1852
+ This function processes text content to identify and format various types of lists,
1853
+ including numbered lists with checkboxes, regular lists, and plan structures.
1854
+ It also handles preprocessing tasks like removing bold formatting from plan titles
1855
+ and removing emojis for clean PDF output.
1856
+
1857
+ Args:
1858
+ text: The text content to process for list formatting
1859
+
1860
+ Returns:
1861
+ Formatted text with properly structured lists and cleaned formatting
1862
+
1863
+ Note:
1864
+ The function performs several preprocessing steps:
1865
+ - Removes bold formatting from plan titles
1866
+ - Removes emojis for PDF output
1867
+ - Identifies and formats checkbox lists
1868
+ - Processes regular text blocks
1869
+ """
1870
+ import re
1871
+
1872
+ # Preprocess to remove bold formatting from plan titles
1873
+ # Remove **Plan:**, **Updated Plan:**, **Completed Plan:**, etc.
1874
+ text = re.sub(r"\*\*([Pp]lan|Updated [Pp]lan|Completed [Pp]lan|Final [Pp]lan):\*\*", r"\1:", text)
1875
+ # Also handle cases without colons
1876
+ text = re.sub(r"\*\*([Pp]lan|Updated [Pp]lan|Completed [Pp]lan|Final [Pp]lan)\*\*", r"\1", text)
1877
+ # Handle any other bold formatting patterns for plan titles
1878
+ text = re.sub(r"<strong>([Pp]lan|Updated [Pp]lan|Completed [Pp]lan|Final [Pp]lan):</strong>", r"\1:", text)
1879
+ text = re.sub(r"<strong>([Pp]lan|Updated [Pp]lan|Completed [Pp]lan|Final [Pp]lan)</strong>", r"\1", text)
1880
+
1881
+ # Remove emojis from the text for markdown/PDF output
1882
+ text = remove_emojis_from_text(text)
1883
+
1884
+ lines = text.split("\n")
1885
+ list_blocks = identify_list_blocks(lines)
1886
+
1887
+ # Process each block
1888
+ result_blocks = []
1889
+ for block_text, is_checkbox_list in list_blocks:
1890
+ if is_checkbox_list:
1891
+ result_blocks.append(format_single_list(block_text))
1892
+ else:
1893
+ result_blocks.append(block_text)
1894
+
1895
+ return "\n".join(result_blocks)
1896
+
1897
+
1898
+ def identify_list_blocks(lines: list) -> list[tuple[str, bool]]:
1899
+ """Identify blocks of text that contain lists.
1900
+
1901
+ This function analyzes a list of text lines to identify contiguous blocks
1902
+ that contain numbered lists with checkboxes. It groups lines into blocks
1903
+ and marks whether each block contains a checkbox list or regular text.
1904
+
1905
+ Args:
1906
+ lines: List of text lines to analyze
1907
+
1908
+ Returns:
1909
+ List of tuples containing (block_text, is_checkbox_list) where:
1910
+ - block_text: The text content of the block
1911
+ - is_checkbox_list: True if the block contains numbered items with checkboxes
1912
+
1913
+ Note:
1914
+ The function looks for patterns like "1. [ ]", "2. [✓]", "3. [✗]" to
1915
+ identify checkbox sequences and groups them into separate blocks.
1916
+ """
1917
+ import re
1918
+
1919
+ list_blocks = []
1920
+ current_block = []
1921
+ in_checkbox_sequence = False
1922
+
1923
+ for line in lines:
1924
+ line_stripped = line.strip()
1925
+
1926
+ # Check if this line starts a numbered item with checkbox
1927
+ if re.match(r"^\d+\.\s*\[[ ✓✗]\]", line_stripped):
1928
+ if not in_checkbox_sequence:
1929
+ # Start of a new checkbox sequence
1930
+ if current_block:
1931
+ list_blocks.append(("\n".join(current_block), False))
1932
+ current_block = [line]
1933
+ in_checkbox_sequence = True
1934
+ else:
1935
+ # Continue the sequence
1936
+ current_block.append(line)
1937
+ else:
1938
+ if in_checkbox_sequence:
1939
+ # End of checkbox sequence
1940
+ if current_block:
1941
+ list_blocks.append(("\n".join(current_block), True))
1942
+ current_block = []
1943
+ in_checkbox_sequence = False
1944
+ current_block.append(line)
1945
+
1946
+ # Handle the last block
1947
+ if current_block:
1948
+ if in_checkbox_sequence:
1949
+ list_blocks.append(("\n".join(current_block), True))
1950
+ else:
1951
+ list_blocks.append(("\n".join(current_block), False))
1952
+
1953
+ return list_blocks
1954
+
1955
+
1956
+ def format_single_list(text: str) -> str:
1957
+ """Format a single list block with checkboxes and plan titles.
1958
+
1959
+ This function processes a text block that may contain numbered lists with
1960
+ checkboxes and plan titles. It converts checkbox symbols to HTML list items
1961
+ and wraps the content in a styled container with appropriate CSS classes.
1962
+
1963
+ Args:
1964
+ text: The text block to format, potentially containing numbered lists
1965
+
1966
+ Returns:
1967
+ HTML string containing either a formatted list with plan title or
1968
+ regular text if no list items are found
1969
+
1970
+ Note:
1971
+ The function recognizes plan titles like "Plan", "Updated Plan", "Completed Plan"
1972
+ and converts checkbox symbols (✓, ✗) to HTML format ([x], [ ]).
1973
+ """
1974
+ import re
1975
+
1976
+ lines = text.split("\n")
1977
+ list_items = []
1978
+ has_list_items = False
1979
+ plan_title = "Plan" # Default title
1980
+
1981
+ for line in lines:
1982
+ line = line.strip()
1983
+ if not line:
1984
+ continue
1985
+
1986
+ # Check for plan title patterns
1987
+ if re.match(r"^(Plan|Updated Plan|Completed Plan)$", line, re.IGNORECASE):
1988
+ plan_title = line
1989
+ continue
1990
+
1991
+ # Check for numbered lists with checkboxes (1. [ ] or 1. [✓] or 1. [✗])
1992
+ if re.match(r"^\d+\.\s*\[[ ✓✗]\]", line):
1993
+ has_list_items = True
1994
+ # Extract the content after the checkbox
1995
+ content = re.sub(r"^\d+\.\s*\[[ ✓✗]\]\s*", "", line)
1996
+
1997
+ # Replace checkbox symbols with text format
1998
+ if "[✓]" in line:
1999
+ list_items.append(f"<li><strong>[x]</strong> {content}</li>")
2000
+ elif "[✗]" in line:
2001
+ list_items.append(f"<li><strong>[ ]</strong> {content}</li>")
2002
+ else:
2003
+ list_items.append(f"<li><strong>[ ]</strong> {content}</li>")
2004
+ else:
2005
+ # Regular text - add as is (don't convert to list items)
2006
+ list_items.append(line)
2007
+
2008
+ if has_list_items and list_items:
2009
+ # This is a list - return with container div and styled title
2010
+ return f"""<div class="title-text plan">
2011
+ <div class="title-text-header">
2012
+ <span class="plan-title">{plan_title}</span>
2013
+ </div>
2014
+ <div class="title-text-content">
2015
+ <ul>
2016
+ {chr(10).join(list_items)}
2017
+ </ul>
2018
+ </div>
2019
+ </div>"""
2020
+ else:
2021
+ # Regular text
2022
+ return "\n".join(list_items)
2023
+
2024
+
2025
+ def convert_markdown_to_pdf(markdown_path: str, pdf_path: str) -> None:
2026
+ """Convert markdown file to PDF using weasyprint or fallback libraries.
2027
+
2028
+ This function converts a markdown file to PDF format using multiple fallback
2029
+ strategies. It prioritizes weasyprint for better layout control, then falls back
2030
+ to markdown2pdf and finally pandoc if the preferred libraries are not available.
2031
+
2032
+ Args:
2033
+ markdown_path: Path to the input markdown file
2034
+ pdf_path: Path where the output PDF file should be saved
2035
+
2036
+ Raises:
2037
+ ImportError: If no PDF conversion library is available
2038
+ Exception: If PDF conversion fails for any other reason
2039
+
2040
+ Note:
2041
+ The function uses minimal markdown extensions for better performance
2042
+ and applies custom CSS styling for consistent formatting.
2043
+ """
2044
+ try:
2045
+ # Try weasyprint first (better for complex layouts)
2046
+ from weasyprint import HTML
2047
+ from weasyprint.text.fonts import FontConfiguration
2048
+
2049
+ # Read markdown content
2050
+ with open(markdown_path, encoding="utf-8") as f:
2051
+ markdown_content = f.read()
2052
+
2053
+ # Convert markdown to HTML with minimal extensions for better performance
2054
+ import markdown
2055
+
2056
+ # Use minimal extensions to improve performance
2057
+ html_content = markdown.markdown(
2058
+ markdown_content,
2059
+ extensions=["fenced_code"], # Removed codehilite for better performance
2060
+ )
2061
+
2062
+ # Add CSS styling
2063
+ css_content = get_pdf_css_content()
2064
+
2065
+ # Create HTML document
2066
+ html_doc = f"""
2067
+ <!DOCTYPE html>
2068
+ <html>
2069
+ <head>
2070
+ <meta charset="utf-8">
2071
+ <title>Biomni Conversation History</title>
2072
+ <style>{css_content}</style>
2073
+ </head>
2074
+ <body>
2075
+ {html_content}
2076
+ </body>
2077
+ </html>
2078
+ """
2079
+
2080
+ # Convert to PDF with performance optimizations
2081
+ font_config = FontConfiguration()
2082
+ html_obj = HTML(string=html_doc)
2083
+ html_obj.write_pdf(pdf_path, font_config=font_config, optimize_images=True)
2084
+
2085
+ except ImportError:
2086
+ # Fallback to markdown2pdf if weasyprint is not available
2087
+ try:
2088
+ from markdown2pdf import markdown2pdf
2089
+
2090
+ markdown2pdf(markdown_path, pdf_path)
2091
+ except ImportError:
2092
+ # Final fallback - try using pandoc if available
2093
+ import subprocess
2094
+
2095
+ try:
2096
+ subprocess.run(["pandoc", markdown_path, "-o", pdf_path], check=True)
2097
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
2098
+ raise ImportError(
2099
+ "No PDF conversion library available. Please install weasyprint, markdown2pdf, or pandoc."
2100
+ ) from e
2101
+ except Exception as e:
2102
+ raise Exception(f"PDF conversion failed: {e}") from e
2103
+
2104
+
2105
+ def get_pdf_css_content() -> str:
2106
+ """Get the CSS content for PDF generation.
2107
+
2108
+ This function returns a comprehensive CSS stylesheet designed specifically
2109
+ for PDF generation from markdown content. It includes styling for all
2110
+ HTML elements that may appear in the converted markdown, with optimized
2111
+ typography, spacing, and layout for print media.
2112
+
2113
+ Returns:
2114
+ CSS string containing all styles needed for PDF generation
2115
+
2116
+ Note:
2117
+ The CSS includes styles for:
2118
+ - Typography and font families
2119
+ - Headings and text formatting
2120
+ - Code blocks and syntax highlighting
2121
+ - Tables and lists
2122
+ - Custom classes for tool calls, observations, and plans
2123
+ - Print-optimized spacing and layout
2124
+ """
2125
+ return """
2126
+ body {
2127
+ /* Previously: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif, 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Twemoji', 'EmojiOne Color' */
2128
+ font-family: sans-serif;
2129
+ font-size: 9pt;
2130
+ line-height: 1.4;
2131
+ max-width: 800px;
2132
+ margin: 0 auto;
2133
+ padding: 15px;
2134
+ color: #333;
2135
+ }
2136
+ h1, h2, h3, h4, h5, h6 {
2137
+ /* Previously: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif, 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Twemoji', 'EmojiOne Color' */
2138
+ font-family: sans-serif;
2139
+ color: #2c3e50;
2140
+ margin-top: 1em;
2141
+ margin-bottom: 0.5em;
2142
+ }
2143
+ h1 {
2144
+ border-bottom: 2px solid #3498db;
2145
+ padding-bottom: 8px;
2146
+ font-size: 16pt;
2147
+ }
2148
+ h2 {
2149
+ border-bottom: 1px solid #bdc3c7;
2150
+ padding-bottom: 3px;
2151
+ font-size: 14pt;
2152
+ }
2153
+ h3 {
2154
+ font-size: 12pt;
2155
+ }
2156
+ h4 {
2157
+ font-size: 10pt;
2158
+ margin-top: 0.8em;
2159
+ margin-bottom: 0.3em;
2160
+ }
2161
+ h5, h6 {
2162
+ font-size: 9pt;
2163
+ margin-top: 0.6em;
2164
+ margin-bottom: 0.2em;
2165
+ }
2166
+ code {
2167
+ background-color: #f8f9fa;
2168
+ padding: 1px 3px;
2169
+ border-radius: 2px;
2170
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
2171
+ font-size: 8pt;
2172
+ white-space: pre-wrap;
2173
+ word-wrap: break-word;
2174
+ }
2175
+ pre {
2176
+ background-color: #f8f9fa;
2177
+ padding: 10px;
2178
+ border-radius: 3px;
2179
+ overflow-x: auto;
2180
+ border-left: 3px solid #3498db;
2181
+ white-space: pre-wrap;
2182
+ word-wrap: break-word;
2183
+ font-size: 8pt;
2184
+ margin: 0.5em 0;
2185
+ }
2186
+ pre code {
2187
+ background-color: transparent;
2188
+ padding: 0;
2189
+ border-radius: 0;
2190
+ font-size: 8pt;
2191
+ }
2192
+ /* Code header styling */
2193
+ strong {
2194
+ font-size: 9pt;
2195
+ font-weight: normal;
2196
+ color: #6c757d;
2197
+ font-style: italic;
2198
+ }
2199
+ blockquote {
2200
+ border-left: 3px solid #bdc3c7;
2201
+ margin: 0.5em 0;
2202
+ padding-left: 15px;
2203
+ color: #7f8c8d;
2204
+ font-size: 8pt;
2205
+ }
2206
+ table {
2207
+ border-collapse: collapse;
2208
+ width: 100%;
2209
+ margin: 0.5em 0;
2210
+ font-size: 8pt;
2211
+ }
2212
+ th, td {
2213
+ border: 1px solid #bdc3c7;
2214
+ padding: 4px 8px;
2215
+ text-align: left;
2216
+ }
2217
+ th {
2218
+ background-color: #ecf0f1;
2219
+ font-weight: bold;
2220
+ }
2221
+ img {
2222
+ max-width: 100%;
2223
+ height: auto;
2224
+ display: block;
2225
+ margin: 10px auto;
2226
+ border: 1px solid #ddd;
2227
+ border-radius: 3px;
2228
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
2229
+ }
2230
+ p {
2231
+ /* Previously: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif, 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Twemoji', 'EmojiOne Color' */
2232
+ font-family: sans-serif;
2233
+ margin: 0.3em 0;
2234
+ }
2235
+ /* Tool call highlighting - matching observation and code formatting */
2236
+ .tool-call-highlight {
2237
+ background-color: #f8f9fa;
2238
+ border: 1px solid #e9ecef;
2239
+ border-radius: 3px;
2240
+ padding: 0;
2241
+ margin: 10px 0;
2242
+ overflow: hidden;
2243
+ }
2244
+ .tool-call-header {
2245
+ background-color: #e9ecef;
2246
+ color: #495057;
2247
+ padding: 8px 12px;
2248
+ margin: 0;
2249
+ font-weight: normal;
2250
+ font-size: 9pt;
2251
+ font-style: italic;
2252
+ border-bottom: 1px solid #dee2e6;
2253
+ }
2254
+ .tool-call-input {
2255
+ background-color: #f8f9fa;
2256
+ border: none;
2257
+ border-radius: 0;
2258
+ padding: 10px 12px;
2259
+ margin: 0;
2260
+ color: #333;
2261
+ font-size: 8pt;
2262
+ line-height: 1.4;
2263
+ }
2264
+ .tool-call-input strong {
2265
+ color: #495057;
2266
+ font-weight: normal;
2267
+ font-size: 8pt;
2268
+ font-style: italic;
2269
+ }
2270
+ .tool-call-input pre {
2271
+ background-color: #f8f9fa;
2272
+ border: 1px solid #e9ecef;
2273
+ border-radius: 3px;
2274
+ padding: 10px;
2275
+ margin: 0;
2276
+ font-size: 8pt;
2277
+ line-height: 1.4;
2278
+ overflow-x: auto;
2279
+ white-space: pre-wrap;
2280
+ word-wrap: break-word;
2281
+ }
2282
+ .tool-call-input code {
2283
+ background-color: transparent;
2284
+ padding: 0;
2285
+ border-radius: 0;
2286
+ font-size: 8pt;
2287
+ color: #2c3e50;
2288
+ }
2289
+ .tools-used {
2290
+ background-color: #f8f9fa;
2291
+ border-top: 1px solid #dee2e6;
2292
+ padding: 8px 12px;
2293
+ margin: 0;
2294
+ font-size: 8pt;
2295
+ color: #6c757d;
2296
+ }
2297
+ .tools-used strong {
2298
+ color: #6c757d;
2299
+ font-weight: normal;
2300
+ font-size: 8pt;
2301
+ font-style: italic;
2302
+ }
2303
+ /* Title-text styling - unified for observations, plans, and solutions */
2304
+ .title-text {
2305
+ background-color: #f8f9fa;
2306
+ border: 1px solid #e9ecef;
2307
+ border-radius: 3px;
2308
+ padding: 0;
2309
+ margin: 10px 0;
2310
+ overflow: hidden;
2311
+ }
2312
+ .title-text-header {
2313
+ background-color: #e9ecef;
2314
+ color: #495057;
2315
+ padding: 8px 12px;
2316
+ margin: 0;
2317
+ font-weight: normal;
2318
+ font-size: 9pt;
2319
+ font-style: italic;
2320
+ border-bottom: 1px solid #dee2e6;
2321
+ }
2322
+ .title-text-header strong {
2323
+ color: #495057;
2324
+ font-weight: normal;
2325
+ font-size: 9pt;
2326
+ font-style: italic;
2327
+ }
2328
+ .title-text-content {
2329
+ background-color: #f8f9fa;
2330
+ border: none;
2331
+ border-radius: 0;
2332
+ padding: 10px 12px;
2333
+ margin: 0;
2334
+ color: #333;
2335
+ font-size: 8pt;
2336
+ line-height: 1.4;
2337
+ }
2338
+ /* Plan-specific styling - soft blue pastel */
2339
+ .title-text.plan {
2340
+ background-color: #e3f2fd;
2341
+ border-color: #bbdefb;
2342
+ }
2343
+ .title-text.plan .title-text-header {
2344
+ background-color: #bbdefb;
2345
+ color: #1976d2;
2346
+ }
2347
+ .title-text.plan .title-text-content {
2348
+ background-color: #e3f2fd;
2349
+ }
2350
+ .plan-title {
2351
+ font-style: italic;
2352
+ font-weight: normal;
2353
+ color: #1565c0;
2354
+ text-shadow: 0 1px 2px rgba(0,0,0,0.1);
2355
+ }
2356
+ .plan-title strong {
2357
+ font-weight: normal;
2358
+ }
2359
+ /* Code execution-specific styling - matching title-text styling */
2360
+ .tool-call-highlight {
2361
+ background-color: #f8f9fa;
2362
+ border-color: #e9ecef;
2363
+ }
2364
+ .tool-call-header {
2365
+ background-color: #e9ecef;
2366
+ color: #495057;
2367
+ }
2368
+ .tool-call-input {
2369
+ background-color: #f8f9fa;
2370
+ color: #333;
2371
+ }
2372
+ /* Observation-specific styling - soft purple pastel */
2373
+ .title-text.observation {
2374
+ background-color: #f3e5f5;
2375
+ border-color: #e1bee7;
2376
+ }
2377
+ .title-text.observation .title-text-header {
2378
+ background-color: #e1bee7;
2379
+ color: #7b1fa2;
2380
+ }
2381
+ .title-text.observation .title-text-content {
2382
+ background-color: #f3e5f5;
2383
+ }
2384
+ /* Summary and solution-specific styling - soft orange pastel, no overlay */
2385
+ .title-text.summary {
2386
+ background-color: #fff3e0;
2387
+ border-color: #ffcc02;
2388
+ }
2389
+ .title-text.summary .title-text-header {
2390
+ background-color: #ffcc02;
2391
+ color: #f57c00;
2392
+ }
2393
+ .title-text.summary .title-text-content {
2394
+ background-color: #fff3e0;
2395
+ }
2396
+ .title-text-content ul {
2397
+ background-color: transparent;
2398
+ border: none;
2399
+ border-radius: 0;
2400
+ padding: 0;
2401
+ margin: 0;
2402
+ color: #333;
2403
+ font-size: 8pt;
2404
+ line-height: 1.4;
2405
+ }
2406
+ .title-text-content li {
2407
+ margin: 3px 0;
2408
+ color: #333;
2409
+ }
2410
+ .title-text-content li strong {
2411
+ color: #495057;
2412
+ font-weight: normal;
2413
+ font-size: 8pt;
2414
+ font-style: italic;
2415
+ }
2416
+ .title-text-content li code {
2417
+ background-color: #e9ecef;
2418
+ color: #333;
2419
+ padding: 1px 3px;
2420
+ border-radius: 2px;
2421
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
2422
+ font-size: 7pt;
2423
+ }
2424
+ .title-text-content pre {
2425
+ background-color: #f8f9fa;
2426
+ border: 1px solid #e9ecef;
2427
+ border-radius: 3px;
2428
+ padding: 10px;
2429
+ margin: 0;
2430
+ font-size: 8pt;
2431
+ line-height: 1.4;
2432
+ overflow-x: auto;
2433
+ white-space: pre-wrap;
2434
+ word-wrap: break-word;
2435
+ }
2436
+ .title-text-content code {
2437
+ background-color: transparent;
2438
+ padding: 0;
2439
+ border-radius: 0;
2440
+ font-size: 8pt;
2441
+ color: #2c3e50;
2442
+ }
2443
+ /* Parsing error display styling */
2444
+ .parsing-error-box {
2445
+ background-color: #ffebee;
2446
+ border: 1px solid #f44336;
2447
+ border-radius: 4px;
2448
+ padding: 8px 12px;
2449
+ margin: 8px 0;
2450
+ font-size: 9pt;
2451
+ color: #c62828;
2452
+ box-shadow: 0 2px 4px rgba(244, 67, 54, 0.1);
2453
+ }
2454
+ .parsing-error-header {
2455
+ font-weight: bold;
2456
+ margin-bottom: 4px;
2457
+ color: #d32f2f;
2458
+ }
2459
+ .parsing-error-content {
2460
+ font-family: 'Courier New', monospace;
2461
+ background-color: #ffcdd2;
2462
+ padding: 4px 6px;
2463
+ border-radius: 2px;
2464
+ margin-top: 4px;
2465
+ font-size: 8pt;
2466
+ white-space: pre-wrap;
2467
+ word-wrap: break-word;
2468
+ }
2469
+ """
Biomni/biomni/version.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = "0.0.8" # pragma: no cover
Biomni/license_info.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Biomni Data Source License Information
2
+
3
+ This document provides an overview of the data sources used by Biomni, their licenses, and suitability for internal hosting and commercial use.
4
+
5
+ A significant portion of the data used in Biomni requires a commercial license for any commercial application. Several datasets are explicitly licensed for non-commercial use only, which would prohibit their use in a commercial product without a separate agreement. Before proceeding with any commercial use, a thorough legal review of the licenses for each dataset you intend to use is strongly recommended.
6
+
7
+ ## Data License and Internal Hosting Analysis
8
+
9
+ | Data Source Category | Example Files | License | Internal Hosting | Source |
10
+ | :--- | :--- | :--- | :--- | :--- |
11
+ | **COSMIC** | `Cosmic_*.csv`, `Cosmic_*.parquet` | Requires commercial license for commercial use. | Yes, with a valid commercial license. | [Sanger Institute](https://cancer.sanger.ac.uk/cosmic) |
12
+ | **BindingDB** | `BindingDB_All_202409.tsv` | Custom, non-commercial use granted. Commercial use requires a license. | Yes, with a commercial license. | [BindingDB](https://www.bindingdb.org) |
13
+ | **Broad Repurposing Hub** | `broad_repurposing_hub_*.parquet` | CC BY 4.0 | Yes | [Broad Institute](https://www.broadinstitute.org/drug-repurposing-hub) |
14
+ | **DDInter** | `ddinter_*.csv` | CC BY-NC-SA 4.0 | No, non-commercial use only. | [DDInter](http://ddinter.scbdd.com/) |
15
+ | **DisGeNET** | `DisGeNET.parquet` | CC BY-NC-SA 4.0 | No, non-commercial use only. | [DisGeNET](https://www.disgenet.org/) |
16
+ | **Enamine** | `enamine_cloud_library_smiles.pkl` | Proprietary. Requires license for screening. | Yes, with a valid license. | [Enamine](https://enamine.net/) |
17
+ | **EveBio** | `evebio_*.csv` | Appears to be proprietary data from EveBio. | Requires permission from EveBio. | EveBio |
18
+ | **Gene Ontology (GO)** | `go-plus.json` | CC BY 4.0 | Yes | [Gene Ontology Consortium](http://geneontology.org/) |
19
+ | **GTEx** | `gtex_tissue_gene_tpm.parquet` | dbGaP controlled access. | Yes, with authorization. | [GTEx Portal](https://gtexportal.org/) |
20
+ | **Human Protein Atlas** | `proteinatlas.tsv` | CC BY-SA 3.0 | Yes | [Human Protein Atlas](https://www.proteinatlas.org/) |
21
+ | **MSigDB** | `msigdb_human_*.parquet` | Custom, requires license for commercial use. | Yes, with a license. | [Broad Institute](https://www.gsea-msigdb.org/gsea/msigdb) |
22
+ | **OMIM** | `omim.parquet` | Custom, requires license for commercial use. | Yes, with a license. | [OMIM](https://omim.org/) |
23
+ | **BioGRID** | `affinity_capture-ms.parquet`, etc. | OSL 3.0 | Yes | [BioGRID](https://thebiogrid.org/) |
24
+ | **CZI Cell Census** | `czi_census_datasets_v4.parquet` | CC BY 4.0 | Yes | [Chan Zuckerberg Initiative](https://cellxgene.cziscience.com/census) |
25
+ | **DepMap** | `DepMap_*.csv` | CC BY 4.0 | Yes | [Broad Institute DepMap](https://depmap.org/) |
26
+ | **Genebass** | `genebass_*.pkl` | ODC-By v1.0 | Yes | [Genebass](https://genebass.org/) |
27
+ | **GWAS Catalog** | `gwas_catalog.pkl` | Apache 2.0 | Yes | [EBI GWAS Catalog](https://www.ebi.ac.uk/gwas/) |
28
+ | **HPO** | `hp.obo` | Custom, free for all uses. | Yes | [Human Phenotype Ontology](https://hpo.jax.org/) |
29
+ | **McPAS-TCR** | `McPAS-TCR.parquet` | CC BY-NC-SA 4.0 | No, non-commercial use only. | [McPAS-TCR](http://friedmanlab.weizmann.ac.il/McPAS-TCR/) |
30
+ | **miRDB** | `miRDB_v6.0_results.parquet` | Custom, free for non-commercial use. | No, non-commercial use only. | [miRDB](http://mirdb.org/) |
31
+ | **miRTarBase** | `miRTarBase_*.parquet` | CC BY-NC 4.0 | No, non-commercial use only. | [miRTarBase](https://mirtarbase.cuhk.edu.cn/) |
32
+ | **MouseMine** | `mousemine_*.parquet` | CC BY 4.0 | Yes | [MouseMine](http://www.mousemine.org/) |
33
+ | **P-HIPSTER** | `Virus-Host_PPI_P-HIPSTER_2020.parquet` | CC BY 4.0 | Yes | [P-HIPSTER](http://phipster.org) |
34
+ | **TXGNN** | `txgnn_*.pkl` | MIT License | Yes | - |
35
+
36
+ ## Configuration for Commercial Use
37
+
38
+ To manage which datasets are used based on licensing, Biomni provides a configuration option. You can set the `commercial_mode` flag to `True` in your configuration to automatically exclude datasets that are not licensed for commercial use.
39
+
40
+ ### Usage
41
+
42
+ ```python
43
+ from biomni.agent import A1
44
+
45
+ # For commercial use (excludes non-commercial datasets)
46
+ agent = A1(commercial_mode=True)
47
+
48
+ # For academic/research use (includes all datasets)
49
+ agent = A1(commercial_mode=False) # default
50
+ ```
51
+
52
+ This configuration automatically selects the appropriate data environment description file and ensures compliance with licensing requirements.
Biomni/pyproject.toml ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "biomni"
7
+ dynamic = ["version"]
8
+ description = "Biomni"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ license-files = ["LICENSE"]
12
+ authors = [
13
+ {name = "Biomni Team", email = "kexinh@cs.stanford.edu"}
14
+ ]
15
+ requires-python = ">=3.11"
16
+ dependencies = ["pydantic", "langchain", "python-dotenv"]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/snap-stanford/biomni"
20
+ Repository = "https://github.com/snap-stanford/biomni"
21
+
22
+ [project.optional-dependencies]
23
+ gradio = ["gradio>=5.0,<6.0"]
24
+
25
+ [tool.setuptools]
26
+ include-package-data = true
27
+
28
+ [tool.setuptools.dynamic]
29
+ version = {attr = "biomni.version.__version__"}
30
+
31
+ [tool.setuptools.packages.find]
32
+ exclude = ["test*", "tutorials*"]
33
+
34
+ [tool.ruff]
35
+ src = ["src"]
36
+ line-length = 120
37
+
38
+ [tool.ruff.format]
39
+ docstring-code-format = true
40
+
41
+ [tool.ruff.lint]
42
+ select = [
43
+ "F", # Errors detected by Pyflakes
44
+ "E", # Error detected by Pycodestyle
45
+ "W", # Warning detected by Pycodestyle
46
+ "I", # isort
47
+ #"D", # pydocstyle
48
+ "B", # flake8-bugbear
49
+ "TID", # flake8-tidy-imports
50
+ "C4", # flake8-comprehensions
51
+ "BLE", # flake8-blind-except
52
+ "UP", # pyupgrade
53
+ "RUF100", # Report unused noqa directives
54
+ "TCH", # Typing imports
55
+ #"NPY", # Numpy specific rules
56
+ #"PTH", # Use pathlib
57
+ #"PL" # pylint
58
+ ]
59
+ ignore = [
60
+ # line too long -> we accept long comment lines; black gets rid of long code lines
61
+ "E501",
62
+ # Do not assign a lambda expression, use a def -> lambda expression assignments are convenient
63
+ "E731",
64
+ # allow I, O, l as variable names -> I is the identity matrix
65
+ "E741",
66
+ # Missing docstring in public package
67
+ "D104",
68
+ # Missing docstring in public module
69
+ "D100",
70
+ # Missing docstring in __init__
71
+ "D107",
72
+ # Errors from function calls in argument defaults. These are fine when the result is immutable.
73
+ "B008",
74
+ # __magic__ methods are are often self-explanatory, allow missing docstrings
75
+ "D105",
76
+ # first line should end with a period [Bug: doesn't work with single-line docstrings]
77
+ "D400",
78
+ # First line should be in imperative mood; try rephrasing
79
+ "D401",
80
+ ## Disable one in each pair of mutually incompatible rules
81
+ # We don’t want a blank line before a class docstring
82
+ "D203",
83
+ # We want docstrings to start immediately after the opening triple quote
84
+ "D213",
85
+ # camcelcase imported as lowercase
86
+ "N813",
87
+ # module import not at top level of file
88
+ "E402",
89
+ # Too many arguments in function definition
90
+ "PLR0913",
91
+ # Too many branches
92
+ "PLR0912",
93
+ # magic value used in comparison
94
+ "PLR2004",
95
+ # Too many statements
96
+ "PLR0915",
97
+ # Extracting value from dictionary without calling `.items()` - clashes with sim
98
+ "PLC0206",
99
+ # import should be at top of file
100
+ "PLC0415",
101
+ # do not catch blind exception
102
+ "BLE001",
103
+ # strip with multi characters
104
+ "B005"
105
+ ]
cloningscenarios_all_models_cloning_20260524_163903.log ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ RUN_ID=cloning_20260524_163903
2
+ Logs: /225040511/project/cloningscenarios_logs/cloning_20260524_163903
3
+ Biomanus PID: 115392
4
+ Biomni scales PID: 115393
5
+ Biomni-ReAct PID: 115394
6
+ React+code PID: 115395
7
+ All CloningScenarios jobs finished for RUN_ID=cloning_20260524_163903
ollama.log ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ nohup: ignoring input
2
+ time=2025-12-21T13:03:10.395Z level=INFO source=routes.go:1554 msg="server config" env="map[CUDA_VISIBLE_DEVICES: GGML_VK_VISIBLE_DEVICES: GPU_DEVICE_ORDINAL: HIP_VISIBLE_DEVICES: HSA_OVERRIDE_GFX_VERSION: HTTPS_PROXY: HTTP_PROXY: NO_PROXY: OLLAMA_CONTEXT_LENGTH:4096 OLLAMA_DEBUG:INFO OLLAMA_FLASH_ATTENTION:false OLLAMA_GPU_OVERHEAD:0 OLLAMA_HOST:http://127.0.0.1:11434 OLLAMA_KEEP_ALIVE:5m0s OLLAMA_KV_CACHE_TYPE: OLLAMA_LLM_LIBRARY: OLLAMA_LOAD_TIMEOUT:5m0s OLLAMA_MAX_LOADED_MODELS:0 OLLAMA_MAX_QUEUE:512 OLLAMA_MODELS:/225040511/project/ollama/models OLLAMA_MULTIUSER_CACHE:false OLLAMA_NEW_ENGINE:false OLLAMA_NOHISTORY:false OLLAMA_NOPRUNE:false OLLAMA_NUM_PARALLEL:1 OLLAMA_ORIGINS:[http://localhost https://localhost http://localhost:* https://localhost:* http://127.0.0.1 https://127.0.0.1 http://127.0.0.1:* https://127.0.0.1:* http://0.0.0.0 https://0.0.0.0 http://0.0.0.0:* https://0.0.0.0:* app://* file://* tauri://* vscode-webview://* vscode-file://*] OLLAMA_REMOTES:[ollama.com] OLLAMA_SCHED_SPREAD:false OLLAMA_VULKAN:false ROCR_VISIBLE_DEVICES: http_proxy: https_proxy: no_proxy:]"
3
+ time=2025-12-21T13:03:10.395Z level=INFO source=images.go:493 msg="total blobs: 0"
4
+ time=2025-12-21T13:03:10.396Z level=INFO source=images.go:500 msg="total unused blobs removed: 0"
5
+ time=2025-12-21T13:03:10.396Z level=INFO source=routes.go:1607 msg="Listening on 127.0.0.1:11434 (version 0.13.5)"
6
+ time=2025-12-21T13:03:10.396Z level=INFO source=runner.go:67 msg="discovering available GPUs..."
7
+ time=2025-12-21T13:03:10.397Z level=INFO source=server.go:429 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engine --port 41421"
8
+ time=2025-12-21T13:03:10.589Z level=INFO source=server.go:429 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engine --port 39047"
9
+ time=2025-12-21T13:03:10.687Z level=INFO source=runner.go:106 msg="experimental Vulkan support disabled. To enable, set OLLAMA_VULKAN=1"
10
+ time=2025-12-21T13:03:10.687Z level=INFO source=server.go:429 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engine --port 46809"
11
+ time=2025-12-21T13:03:10.863Z level=INFO source=types.go:42 msg="inference compute" id=GPU-a680880b-cb7f-062f-fa43-501d14dbbd89 filter_id="" library=CUDA compute=8.9 name=CUDA0 description="NVIDIA GeForce RTX 4090" libdirs=ollama,cuda_v12 driver=12.8 pci_id=0000:27:00.0 type=discrete total="48.0 GiB" available="47.4 GiB"
12
+ [GIN] 2025/12/21 - 13:03:21 | 200 | 66.064µs | 127.0.0.1 | HEAD "/"
13
+ [GIN] 2025/12/21 - 13:03:21 | 200 | 522.445µs | 127.0.0.1 | GET "/api/tags"
rerun_base_deepseek_cloningscenarios_50shards.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import subprocess
6
+ import time
7
+ from pathlib import Path
8
+
9
+
10
+ PROJECT_ROOT = Path("/225040511/project")
11
+ PYTHON_BIN = Path(os.getenv("LAB_BENCH_RUNNER_PYTHON", "/225040511/miniconda3/envs/biomni_e1/bin/python"))
12
+ RUNNER = PROJECT_ROOT / "react_code_bioagent_deepseek/react_code_bioagent/labbench_runner.py"
13
+ OUT = Path(
14
+ os.getenv(
15
+ "OUT",
16
+ str(PROJECT_ROOT / "base_llm_labbench/results/cloningscenarios_base_deepseek_cloning_50shards_20260524_174548"),
17
+ )
18
+ )
19
+ SHARD_COUNT = int(os.getenv("SHARD_COUNT", "50"))
20
+ SHARD_CONCURRENCY = int(os.getenv("SHARD_CONCURRENCY", "5"))
21
+ SEED = os.getenv("SEED", "20260514")
22
+
23
+
24
+ def load_env_file(path: Path) -> None:
25
+ if not path.exists():
26
+ return
27
+ for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
28
+ line = raw.strip()
29
+ if not line or line.startswith("#") or "=" not in line:
30
+ continue
31
+ if line.startswith("export "):
32
+ line = line[len("export ") :].strip()
33
+ key, value = line.split("=", 1)
34
+ os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
35
+
36
+
37
+ def count_rows(path: Path) -> int:
38
+ if not path.exists():
39
+ return 0
40
+ return sum(1 for _ in path.open("r", encoding="utf-8", errors="replace"))
41
+
42
+
43
+ def main() -> int:
44
+ for env_file in [
45
+ PROJECT_ROOT / ".env",
46
+ PROJECT_ROOT / "LAB-Bench/.env",
47
+ PROJECT_ROOT / "base_llm_labbench/.env",
48
+ ]:
49
+ load_env_file(env_file)
50
+
51
+ env = os.environ.copy()
52
+ env.setdefault("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")
53
+ env.setdefault("DEEPSEEK_MODEL_NAME", "deepseek-chat")
54
+
55
+ OUT.mkdir(parents=True, exist_ok=True)
56
+ shard_log_dir = OUT / "shard_logs"
57
+ shard_log_dir.mkdir(parents=True, exist_ok=True)
58
+
59
+ result_file = OUT / "cloningscenarios_results.jsonl"
60
+ reasoning_log = OUT / "cloningscenarios_reasoning.log"
61
+ result_file.write_text("", encoding="utf-8")
62
+ reasoning_log.write_text("", encoding="utf-8")
63
+ for old_log in shard_log_dir.glob("base_deepseek_cloningscenarios_shard*.log"):
64
+ old_log.unlink()
65
+
66
+ (OUT / "run_config.txt").write_text(
67
+ "\n".join(
68
+ [
69
+ "model=base DeepSeek",
70
+ f"deepseek_model={env['DEEPSEEK_MODEL_NAME']}",
71
+ "eval=CloningScenarios",
72
+ "dev_size=0",
73
+ "test_size=33",
74
+ f"shard_count={SHARD_COUNT}",
75
+ f"shard_concurrency={SHARD_CONCURRENCY}",
76
+ f"seed={SEED}",
77
+ "",
78
+ ]
79
+ ),
80
+ encoding="utf-8",
81
+ )
82
+
83
+ print(f"output={OUT}", flush=True)
84
+ print(f"model={env['DEEPSEEK_MODEL_NAME']}", flush=True)
85
+ print(f"shard_count={SHARD_COUNT}", flush=True)
86
+ print(f"shard_concurrency={SHARD_CONCURRENCY}", flush=True)
87
+
88
+ running: list[tuple[int, subprocess.Popen[bytes], object]] = []
89
+ failures = 0
90
+ next_shard = 0
91
+
92
+ while next_shard < SHARD_COUNT or running:
93
+ while next_shard < SHARD_COUNT and len(running) < SHARD_CONCURRENCY:
94
+ shard = next_shard
95
+ next_shard += 1
96
+ label = f"base_deepseek_cloningscenarios_shard{shard + 1:02d}of{SHARD_COUNT:02d}"
97
+ log_handle = (shard_log_dir / f"{label}.log").open("wb")
98
+ cmd = [
99
+ str(PYTHON_BIN),
100
+ str(RUNNER),
101
+ "--evals",
102
+ "CloningScenarios",
103
+ "--splits",
104
+ "test",
105
+ "--dev-size",
106
+ "0",
107
+ "--test-size",
108
+ "33",
109
+ "--seed",
110
+ SEED,
111
+ "--n-threads",
112
+ "1",
113
+ "--shard-index",
114
+ str(shard),
115
+ "--shard-count",
116
+ str(SHARD_COUNT),
117
+ "--output-root",
118
+ str(OUT),
119
+ "--compact-results-path",
120
+ str(result_file),
121
+ "--reasoning-log-path",
122
+ str(reasoning_log),
123
+ "--skip-existing-results",
124
+ "--model",
125
+ env["DEEPSEEK_MODEL_NAME"],
126
+ "--base-url",
127
+ env["DEEPSEEK_BASE_URL"],
128
+ ]
129
+ proc = subprocess.Popen(cmd, stdout=log_handle, stderr=subprocess.STDOUT, env=env)
130
+ running.append((shard, proc, log_handle))
131
+ print(f"started shard={shard + 1}/{SHARD_COUNT} pid={proc.pid}", flush=True)
132
+
133
+ time.sleep(2)
134
+ still_running: list[tuple[int, subprocess.Popen[bytes], object]] = []
135
+ for shard, proc, log_handle in running:
136
+ rc = proc.poll()
137
+ if rc is None:
138
+ still_running.append((shard, proc, log_handle))
139
+ continue
140
+ log_handle.close()
141
+ if rc != 0:
142
+ failures += 1
143
+ print(f"finished shard={shard + 1}/{SHARD_COUNT} rc={rc}", flush=True)
144
+ running = still_running
145
+ print(f"progress rows={count_rows(result_file)} active={len(running)} failures={failures}", flush=True)
146
+
147
+ print(f"after_rows={count_rows(result_file)}", flush=True)
148
+ print(f"failures={failures}", flush=True)
149
+ return 0
150
+
151
+
152
+ if __name__ == "__main__":
153
+ raise SystemExit(main())
rerun_base_deepseek_cloningscenarios_50shards.sh ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -u
3
+
4
+ PROJECT_ROOT="/225040511/project"
5
+ PYTHON_BIN="${LAB_BENCH_RUNNER_PYTHON:-/225040511/miniconda3/envs/biomni_e1/bin/python}"
6
+ RUNNER="${PROJECT_ROOT}/react_code_bioagent_deepseek/react_code_bioagent/labbench_runner.py"
7
+ OUT="${OUT:-${PROJECT_ROOT}/base_llm_labbench/results/cloningscenarios_base_deepseek_cloning_50shards_20260524_174548}"
8
+ SHARD_COUNT="${SHARD_COUNT:-50}"
9
+ SHARD_CONCURRENCY="${SHARD_CONCURRENCY:-5}"
10
+ SEED="${SEED:-20260514}"
11
+
12
+ for ENV_FILE in "${PROJECT_ROOT}/.env" "${PROJECT_ROOT}/LAB-Bench/.env" "${PROJECT_ROOT}/base_llm_labbench/.env"; do
13
+ if [[ -f "${ENV_FILE}" ]]; then
14
+ set -a
15
+ # shellcheck disable=SC1090
16
+ source "${ENV_FILE}"
17
+ set +a
18
+ fi
19
+ done
20
+
21
+ export DEEPSEEK_BASE_URL="${DEEPSEEK_BASE_URL:-https://api.deepseek.com/v1}"
22
+ export DEEPSEEK_MODEL_NAME="${DEEPSEEK_MODEL_NAME:-deepseek-chat}"
23
+
24
+ mkdir -p "${OUT}/shard_logs"
25
+ : > "${OUT}/cloningscenarios_results.jsonl"
26
+ : > "${OUT}/cloningscenarios_reasoning.log"
27
+ rm -f "${OUT}/shard_logs"/base_deepseek_cloningscenarios_shard*.log
28
+
29
+ {
30
+ echo "model=base DeepSeek"
31
+ echo "deepseek_model=${DEEPSEEK_MODEL_NAME}"
32
+ echo "eval=CloningScenarios"
33
+ echo "dev_size=0"
34
+ echo "test_size=33"
35
+ echo "shard_count=${SHARD_COUNT}"
36
+ echo "shard_concurrency=${SHARD_CONCURRENCY}"
37
+ echo "seed=${SEED}"
38
+ } > "${OUT}/run_config.txt"
39
+
40
+ echo "output=${OUT}"
41
+ echo "model=${DEEPSEEK_MODEL_NAME}"
42
+ echo "shard_count=${SHARD_COUNT}"
43
+ echo "shard_concurrency=${SHARD_CONCURRENCY}"
44
+
45
+ active=0
46
+ for ((shard=0; shard<SHARD_COUNT; shard++)); do
47
+ label="$(printf "base_deepseek_cloningscenarios_shard%02dof%02d" "$((shard + 1))" "${SHARD_COUNT}")"
48
+ "${PYTHON_BIN}" "${RUNNER}" \
49
+ --evals CloningScenarios \
50
+ --splits test \
51
+ --dev-size 0 \
52
+ --test-size 33 \
53
+ --seed "${SEED}" \
54
+ --n-threads 1 \
55
+ --shard-index "${shard}" \
56
+ --shard-count "${SHARD_COUNT}" \
57
+ --output-root "${OUT}" \
58
+ --compact-results-path "${OUT}/cloningscenarios_results.jsonl" \
59
+ --reasoning-log-path "${OUT}/cloningscenarios_reasoning.log" \
60
+ --skip-existing-results \
61
+ --model "${DEEPSEEK_MODEL_NAME}" \
62
+ --base-url "${DEEPSEEK_BASE_URL}" \
63
+ > "${OUT}/shard_logs/${label}.log" 2>&1 &
64
+ active=$((active + 1))
65
+ if [[ "${active}" -ge "${SHARD_CONCURRENCY}" ]]; then
66
+ wait -n || true
67
+ active=$((active - 1))
68
+ fi
69
+ done
70
+
71
+ while [[ "${active}" -gt 0 ]]; do
72
+ wait -n || true
73
+ active=$((active - 1))
74
+ done
75
+
76
+ rows=0
77
+ if [[ -f "${OUT}/cloningscenarios_results.jsonl" ]]; then
78
+ rows="$(wc -l < "${OUT}/cloningscenarios_results.jsonl")"
79
+ fi
80
+ echo "after_rows=${rows}"
run_base_openai_labbench_three_tasks_50shards.sh ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -u
3
+
4
+ RUN_ID="${RUN_ID:-openai_labbench_$(date -u +%Y%m%d_%H%M%S)}"
5
+ PROJECT_ROOT="/225040511/project"
6
+ PYTHON_BIN="${LAB_BENCH_RUNNER_PYTHON:-/225040511/miniconda3/envs/biomni_e1/bin/python}"
7
+ RUNNER="${PROJECT_ROOT}/react_code_bioagent_deepseek/react_code_bioagent/labbench_runner.py"
8
+ RESULT_ROOT="${PROJECT_ROOT}/base_llm_labbench/results/base_openai_${RUN_ID}"
9
+ LOG_ROOT="${PROJECT_ROOT}/base_llm_labbench/logs/base_openai_${RUN_ID}"
10
+ SHARD_COUNT="${LABBENCH_SHARD_COUNT:-50}"
11
+ SHARD_CONCURRENCY="${LABBENCH_SHARD_CONCURRENCY:-5}"
12
+ SEED="${LABBENCH_SEED:-20260514}"
13
+ OPENAI_MODEL_NAME="${OPENAI_MODEL_NAME:-${OPENAI_MODEL:-gpt-4o-mini}}"
14
+ OPENAI_BASE_URL="${OPENAI_BASE_URL:-https://api.openai.com/v1}"
15
+
16
+ mkdir -p "${RESULT_ROOT}" "${LOG_ROOT}"
17
+
18
+ for ENV_FILE in "${PROJECT_ROOT}/.env" "${PROJECT_ROOT}/LAB-Bench/.env" "${PROJECT_ROOT}/base_llm_labbench/.env"; do
19
+ if [[ -f "${ENV_FILE}" ]]; then
20
+ set -a
21
+ # shellcheck disable=SC1090
22
+ source "${ENV_FILE}"
23
+ set +a
24
+ fi
25
+ done
26
+
27
+ if [[ -z "${OPENAI_API_KEY:-}" ]]; then
28
+ echo "Missing OPENAI_API_KEY. Put it in ${PROJECT_ROOT}/.env or export it before launching." >&2
29
+ exit 2
30
+ fi
31
+
32
+ wait_limited() {
33
+ local -n pid_array="$1"
34
+ if [[ "${#pid_array[@]}" -lt "${SHARD_CONCURRENCY}" ]]; then
35
+ return
36
+ fi
37
+ local pid="${pid_array[0]}"
38
+ wait "${pid}" || true
39
+ pid_array=("${pid_array[@]:1}")
40
+ }
41
+
42
+ count_rows() {
43
+ local file="$1"
44
+ if [[ -f "${file}" ]]; then
45
+ wc -l < "${file}"
46
+ else
47
+ echo 0
48
+ fi
49
+ }
50
+
51
+ run_eval() {
52
+ local eval_name="$1"
53
+ local dev_size="$2"
54
+ local test_size="$3"
55
+ local eval_lower
56
+ eval_lower="$(echo "${eval_name}" | tr '[:upper:]' '[:lower:]')"
57
+ local out="${RESULT_ROOT}/${eval_lower}"
58
+ local result="${out}/${eval_lower}_results.jsonl"
59
+ local reasoning="${out}/${eval_lower}_reasoning.log"
60
+ local shard_log_dir="${out}/shard_logs"
61
+ mkdir -p "${shard_log_dir}"
62
+ touch "${result}" "${reasoning}"
63
+
64
+ {
65
+ echo "run_id=${RUN_ID}"
66
+ echo "model=baseLLM OpenAI"
67
+ echo "openai_model=${OPENAI_MODEL_NAME}"
68
+ echo "eval=${eval_name}"
69
+ echo "dev_size=${dev_size}"
70
+ echo "test_size=${test_size}"
71
+ echo "shard_count=${SHARD_COUNT}"
72
+ echo "shard_concurrency=${SHARD_CONCURRENCY}"
73
+ echo "seed=${SEED}"
74
+ } > "${out}/run_config.txt"
75
+
76
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] eval=${eval_name} before_rows=$(count_rows "${result}")"
77
+ local pids=()
78
+ for ((shard=0; shard<SHARD_COUNT; shard++)); do
79
+ local label
80
+ label="$(printf "base_openai_%s_shard%02dof%02d" "${eval_lower}" "$((shard + 1))" "${SHARD_COUNT}")"
81
+ "${PYTHON_BIN}" "${RUNNER}" \
82
+ --evals "${eval_name}" \
83
+ --splits test \
84
+ --dev-size "${dev_size}" \
85
+ --test-size "${test_size}" \
86
+ --seed "${SEED}" \
87
+ --n-threads 1 \
88
+ --shard-index "${shard}" \
89
+ --shard-count "${SHARD_COUNT}" \
90
+ --output-root "${out}" \
91
+ --compact-results-path "${result}" \
92
+ --reasoning-log-path "${reasoning}" \
93
+ --skip-existing-results \
94
+ --model "${OPENAI_MODEL_NAME}" \
95
+ --base-url "${OPENAI_BASE_URL}" \
96
+ --api-key "${OPENAI_API_KEY}" \
97
+ > "${shard_log_dir}/${label}.log" 2>&1 &
98
+ pids+=("$!")
99
+ wait_limited pids
100
+ done
101
+ for pid in "${pids[@]}"; do wait "${pid}" || true; done
102
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] eval=${eval_name} after_rows=$(count_rows "${result}")"
103
+ }
104
+
105
+ echo "RUN_ID=${RUN_ID}"
106
+ echo "RESULT_ROOT=${RESULT_ROOT}"
107
+ echo "LOG_ROOT=${LOG_ROOT}"
108
+ echo "OPENAI_MODEL_NAME=${OPENAI_MODEL_NAME}"
109
+ echo "SHARD_COUNT=${SHARD_COUNT}"
110
+ echo "SHARD_CONCURRENCY=${SHARD_CONCURRENCY}"
111
+
112
+ run_eval DbQA 45 315 > "${LOG_ROOT}/dbqa.log" 2>&1 &
113
+ pid_dbqa=$!
114
+ run_eval SeqQA 45 315 > "${LOG_ROOT}/seqqa.log" 2>&1 &
115
+ pid_seqqa=$!
116
+ run_eval CloningScenarios 0 33 > "${LOG_ROOT}/cloningscenarios.log" 2>&1 &
117
+ pid_cloning=$!
118
+
119
+ echo "dbqa_pid=${pid_dbqa}"
120
+ echo "seqqa_pid=${pid_seqqa}"
121
+ echo "cloningscenarios_pid=${pid_cloning}"
122
+
123
+ wait "${pid_dbqa}" || true
124
+ wait "${pid_seqqa}" || true
125
+ wait "${pid_cloning}" || true
126
+
127
+ echo "finished RUN_ID=${RUN_ID}"
run_biomni_nomcp_cloningscenarios.sh ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -u
3
+
4
+ RUN_ID="${RUN_ID:-cloning_nomcp_$(date -u +%Y%m%d_%H%M%S)}"
5
+ PYTHON_BIN="${LAB_BENCH_RUNNER_PYTHON:-${BIOMNI_EXECUTION_PYTHON:-/225040511/miniconda3/envs/biomni_e1/bin/python}}"
6
+ BIOMNI_ROOT="/225040511/project/Biomni"
7
+ LAB_BENCH_ROOT="/225040511/project/LAB-Bench"
8
+ RUNNER="${BIOMNI_ROOT}/experiments/lab_bench/run_labbench_with_biomni.py"
9
+ OUT_ROOT="${BIOMNI_ROOT}/experiments/lab_bench/results/biomni_no_mcp_cloningscenarios_${RUN_ID}"
10
+ LOG_ROOT="/225040511/project/cloningscenarios_logs/${RUN_ID}"
11
+ EVAL_NAME="CloningScenarios"
12
+ EVAL_LOWER="cloningscenarios"
13
+ DEV_SIZE="${CLONING_DEV_SIZE:-0}"
14
+ TEST_SIZE="${CLONING_TEST_SIZE:-33}"
15
+ SHARD_COUNT="${CLONING_SHARD_COUNT:-33}"
16
+ SHARD_CONCURRENCY="${CLONING_SHARD_CONCURRENCY:-5}"
17
+ SEED="${CLONING_SEED:-20260514}"
18
+
19
+ mkdir -p "${OUT_ROOT}/agent_runtime" "${OUT_ROOT}/shard_logs/${EVAL_LOWER}" "${LOG_ROOT}"
20
+
21
+ for ENV_FILE in "/225040511/project/.env" "${BIOMNI_ROOT}/.env" "${LAB_BENCH_ROOT}/.env"; do
22
+ if [[ -f "${ENV_FILE}" ]]; then
23
+ set -a
24
+ # shellcheck disable=SC1090
25
+ source "${ENV_FILE}"
26
+ set +a
27
+ fi
28
+ done
29
+
30
+ export BIOMNI_CUSTOM_API_KEY="${BIOMNI_CUSTOM_API_KEY:-${DEEPSEEK_API_KEY:-}}"
31
+ export BIOMNI_CUSTOM_BASE_URL="${BIOMNI_CUSTOM_BASE_URL:-${DEEPSEEK_BASE_URL:-https://api.deepseek.com/v1}}"
32
+ export BIOMNI_LLM="${BIOMNI_LLM:-${DEEPSEEK_MODEL_NAME:-deepseek-chat}}"
33
+ export BIOMNI_SOURCE="${BIOMNI_SOURCE:-Custom}"
34
+
35
+ {
36
+ echo "run_id=${RUN_ID}"
37
+ echo "model=Biomni no MCP"
38
+ echo "eval=${EVAL_NAME}"
39
+ echo "dev_size=${DEV_SIZE}"
40
+ echo "test_size=${TEST_SIZE}"
41
+ echo "shard_count=${SHARD_COUNT}"
42
+ echo "shard_concurrency=${SHARD_CONCURRENCY}"
43
+ echo "seed=${SEED}"
44
+ echo "use_mcp=false"
45
+ echo "use_tool_retriever=false"
46
+ } > "${OUT_ROOT}/run_config.txt"
47
+
48
+ count_rows() {
49
+ local file="$1"
50
+ if [[ -f "${file}" ]]; then
51
+ wc -l < "${file}"
52
+ else
53
+ echo 0
54
+ fi
55
+ }
56
+
57
+ wait_limited() {
58
+ local -n pid_array="$1"
59
+ if [[ "${#pid_array[@]}" -lt "${SHARD_CONCURRENCY}" ]]; then
60
+ return
61
+ fi
62
+ local pid="${pid_array[0]}"
63
+ wait "${pid}" || true
64
+ pid_array=("${pid_array[@]:1}")
65
+ }
66
+
67
+ RESULT_FILE="${OUT_ROOT}/${EVAL_LOWER}_results.jsonl"
68
+ REASONING_LOG="${OUT_ROOT}/${EVAL_LOWER}_reasoning.log"
69
+ touch "${RESULT_FILE}" "${REASONING_LOG}"
70
+
71
+ echo "RUN_ID=${RUN_ID}"
72
+ echo "output=${OUT_ROOT}"
73
+ echo "log_root=${LOG_ROOT}"
74
+ echo "before_rows=$(count_rows "${RESULT_FILE}")"
75
+
76
+ pids=()
77
+ for ((shard=0; shard<SHARD_COUNT; shard++)); do
78
+ label="$(printf "biomni_nomcp_%s_shard%02dof%02d" "${EVAL_LOWER}" "$((shard + 1))" "${SHARD_COUNT}")"
79
+ "${PYTHON_BIN}" "${RUNNER}" \
80
+ --eval "${EVAL_NAME}" \
81
+ --split test \
82
+ --dev-size "${DEV_SIZE}" \
83
+ --test-size "${TEST_SIZE}" \
84
+ --seed "${SEED}" \
85
+ --repeat 1 \
86
+ --n_threads 1 \
87
+ --output-root "${OUT_ROOT}" \
88
+ --agent-root "${OUT_ROOT}/agent_runtime" \
89
+ --run-label "${label}" \
90
+ --shard-index "${shard}" \
91
+ --shard-count "${SHARD_COUNT}" \
92
+ --reasoning-log-path "${REASONING_LOG}" \
93
+ --compact-results-path "${RESULT_FILE}" \
94
+ --compact-output-only \
95
+ --skip-existing-results \
96
+ --no-mcp \
97
+ --no-use-tool-retriever \
98
+ > "${OUT_ROOT}/shard_logs/${EVAL_LOWER}/${label}.log" 2>&1 &
99
+ pids+=("$!")
100
+ wait_limited pids
101
+ done
102
+
103
+ for pid in "${pids[@]}"; do
104
+ wait "${pid}" || true
105
+ done
106
+
107
+ echo "after_rows=$(count_rows "${RESULT_FILE}")"
108
+ echo "finished output=${OUT_ROOT}"
run_cloningscenarios_all_models.sh ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ RUN_ID="${RUN_ID:-cloning_$(date -u +%Y%m%d_%H%M%S)}"
5
+ PYTHON_BIN="${LAB_BENCH_RUNNER_PYTHON:-${BIOMNI_EXECUTION_PYTHON:-/225040511/miniconda3/envs/biomni_e1/bin/python}}"
6
+ EVAL_NAME="CloningScenarios"
7
+ EVAL_LOWER="cloningscenarios"
8
+ DEV_SIZE="${CLONING_DEV_SIZE:-0}"
9
+ TEST_SIZE="${CLONING_TEST_SIZE:-33}"
10
+ SHARD_COUNT="${CLONING_SHARD_COUNT:-33}"
11
+ SHARD_CONCURRENCY="${CLONING_SHARD_CONCURRENCY:-33}"
12
+ SEED="${CLONING_SEED:-20260514}"
13
+ SCALES_TEXT="${CLONING_TOOL_SCALES:-100 500 1000 2000}"
14
+
15
+ PROJECT_ROOT="/225040511/project"
16
+ LAB_BENCH_ROOT="${PROJECT_ROOT}/LAB-Bench"
17
+ BIOMANUS_ROOT="${PROJECT_ROOT}/Biomanus"
18
+ BIOMNI_ROOT="${PROJECT_ROOT}/Biomni"
19
+ BIOMNI_REACT_ROOT="${PROJECT_ROOT}/Biomni-ReAct"
20
+ REACT_CODE_ROOT="${PROJECT_ROOT}/React+code_labbench"
21
+ LOG_ROOT="${PROJECT_ROOT}/cloningscenarios_logs/${RUN_ID}"
22
+
23
+ mkdir -p "${LOG_ROOT}"
24
+
25
+ for ENV_FILE in "${PROJECT_ROOT}/.env" "${BIOMANUS_ROOT}/.env" "${BIOMNI_ROOT}/.env" "${BIOMNI_REACT_ROOT}/.env" "${LAB_BENCH_ROOT}/.env"; do
26
+ if [[ -f "${ENV_FILE}" ]]; then
27
+ set -a
28
+ # shellcheck disable=SC1090
29
+ source "${ENV_FILE}"
30
+ set +a
31
+ fi
32
+ done
33
+
34
+ export DEEPSEEK_BASE_URL="${DEEPSEEK_BASE_URL:-https://api.deepseek.com/v1}"
35
+ export DEEPSEEK_MODEL_NAME="${DEEPSEEK_MODEL_NAME:-deepseek-chat}"
36
+ export BIOMNI_CUSTOM_API_KEY="${BIOMNI_CUSTOM_API_KEY:-${DEEPSEEK_API_KEY:-}}"
37
+ export BIOMNI_CUSTOM_BASE_URL="${BIOMNI_CUSTOM_BASE_URL:-${DEEPSEEK_BASE_URL}}"
38
+ export BIOMNI_LLM="${BIOMNI_LLM:-${DEEPSEEK_MODEL_NAME}}"
39
+ export BIOMNI_SOURCE="${BIOMNI_SOURCE:-Custom}"
40
+ export BIOMNI_REACT_API_KEY="${BIOMNI_REACT_API_KEY:-${DEEPSEEK_API_KEY:-${BIOMNI_CUSTOM_API_KEY:-}}}"
41
+ export BIOMNI_REACT_BASE_URL="${BIOMNI_REACT_BASE_URL:-${DEEPSEEK_BASE_URL}}"
42
+ export BIOMNI_REACT_MODEL="${BIOMNI_REACT_MODEL:-${DEEPSEEK_MODEL_NAME}}"
43
+ export REACT_CODE_API_KEY="${REACT_CODE_API_KEY:-${DEEPSEEK_API_KEY:-${BIOMNI_CUSTOM_API_KEY:-}}}"
44
+ export REACT_CODE_BASE_URL="${REACT_CODE_BASE_URL:-${DEEPSEEK_BASE_URL}}"
45
+ export REACT_CODE_MODEL="${REACT_CODE_MODEL:-${DEEPSEEK_MODEL_NAME}}"
46
+
47
+ if [[ -z "${DEEPSEEK_API_KEY:-}" && -z "${BIOMNI_CUSTOM_API_KEY:-}" && -z "${OPENAI_API_KEY:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
48
+ echo "Missing API key. Set DEEPSEEK_API_KEY, BIOMNI_CUSTOM_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY." >&2
49
+ exit 2
50
+ fi
51
+
52
+ latest_biomanus_graph() {
53
+ local graph
54
+ graph="$(find "${BIOMANUS_ROOT}/graph_outputs" -maxdepth 1 -type d -name 'mcp_generated_graph_all_*' 2>/dev/null | sort | tail -n 1 || true)"
55
+ if [[ -n "${graph}" && -d "${graph}" ]]; then
56
+ echo "${graph}"
57
+ else
58
+ echo "${BIOMANUS_ROOT}/graph_outputs/labbench_dbqa_seqqa_subgraph"
59
+ fi
60
+ }
61
+
62
+ wait_limited() {
63
+ local -n pid_array="$1"
64
+ if [[ "${#pid_array[@]}" -lt "${SHARD_CONCURRENCY}" ]]; then
65
+ return
66
+ fi
67
+ local pid
68
+ pid="${pid_array[0]}"
69
+ wait "${pid}" || true
70
+ pid_array=("${pid_array[@]:1}")
71
+ }
72
+
73
+ run_biomanus() {
74
+ local out_root="${BIOMANUS_ROOT}/experiments/lab_bench/results/cloningscenarios_biomanus_${RUN_ID}"
75
+ local agent_root="${out_root}/agent_runtime"
76
+ local shard_log_dir="${out_root}/shard_logs"
77
+ local result_file="${out_root}/${EVAL_LOWER}_results.jsonl"
78
+ local reasoning_log="${out_root}/${EVAL_LOWER}_reasoning.log"
79
+ local graph_dir
80
+ graph_dir="$(latest_biomanus_graph)"
81
+ mkdir -p "${out_root}" "${agent_root}" "${shard_log_dir}"
82
+ {
83
+ echo "run_id=${RUN_ID}"
84
+ echo "model=Biomanus"
85
+ echo "eval=${EVAL_NAME}"
86
+ echo "dev_size=${DEV_SIZE}"
87
+ echo "test_size=${TEST_SIZE}"
88
+ echo "shard_count=${SHARD_COUNT}"
89
+ echo "shard_concurrency=${SHARD_CONCURRENCY}"
90
+ echo "graph_dir=${graph_dir}"
91
+ } > "${out_root}/run_config.txt"
92
+ local pids=()
93
+ for ((shard=0; shard<SHARD_COUNT; shard++)); do
94
+ local label
95
+ label="$(printf "biomanus_%s_shard%02dof%02d" "${EVAL_LOWER}" "$((shard + 1))" "${SHARD_COUNT}")"
96
+ "${PYTHON_BIN}" "${BIOMANUS_ROOT}/experiments/lab_bench/scripts/run_labbench_with_hypobioos.py" \
97
+ --evals "${EVAL_NAME}" \
98
+ --splits test \
99
+ --dev-size "${DEV_SIZE}" \
100
+ --test-size "${TEST_SIZE}" \
101
+ --seed "${SEED}" \
102
+ --repeat 1 \
103
+ --n_threads 1 \
104
+ --output-root "${out_root}" \
105
+ --agent-root "${agent_root}" \
106
+ --graph-dir "${graph_dir}" \
107
+ --run-label "${label}" \
108
+ --shard-index "${shard}" \
109
+ --shard-count "${SHARD_COUNT}" \
110
+ --reasoning-log-path "${reasoning_log}" \
111
+ --compact-results-path "${result_file}" \
112
+ --compact-output-only \
113
+ --skip-existing-results \
114
+ --use-graph-retriever \
115
+ --use-mcp \
116
+ > "${shard_log_dir}/${label}.log" 2>&1 &
117
+ pids+=("$!")
118
+ wait_limited pids
119
+ done
120
+ for pid in "${pids[@]}"; do wait "${pid}" || true; done
121
+ echo "Biomanus done: ${out_root}"
122
+ }
123
+
124
+ run_biomni_scale() {
125
+ local scale="$1"
126
+ local log="${LOG_ROOT}/biomni_scale_${scale}.log"
127
+ RUN_ID="${RUN_ID}" \
128
+ LAB_BENCH_TOOL_SCALES="${scale}" \
129
+ LAB_BENCH_EVALS="${EVAL_NAME}" \
130
+ LAB_BENCH_DEV_SIZE="${DEV_SIZE}" \
131
+ LAB_BENCH_TEST_SIZE="${TEST_SIZE}" \
132
+ LAB_BENCH_TEST_SHARD_COUNT="${SHARD_COUNT}" \
133
+ LAB_BENCH_SHARD_CONCURRENCY="${SHARD_CONCURRENCY}" \
134
+ WAIT_FOR_EXISTING_LABBENCH=0 \
135
+ "${BIOMNI_ROOT}/experiments/lab_bench/run_labbench_tool_scales_biomni.sh" > "${log}" 2>&1
136
+ echo "Biomni scale ${scale} done"
137
+ }
138
+
139
+ run_biomni_scales() {
140
+ local scale_pids=()
141
+ local scale
142
+ read -r -a scales <<< "${SCALES_TEXT}"
143
+ for scale in "${scales[@]}"; do
144
+ run_biomni_scale "${scale}" &
145
+ scale_pids+=("$!")
146
+ done
147
+ for pid in "${scale_pids[@]}"; do wait "${pid}" || true; done
148
+ }
149
+
150
+ run_biomni_react() {
151
+ local out_root="${BIOMNI_REACT_ROOT}/LAB-bench/cloningscenarios_${RUN_ID}"
152
+ local shard_log_dir="${out_root}/shard_logs"
153
+ local result_file="${out_root}/${EVAL_LOWER}_results.jsonl"
154
+ local reasoning_log="${out_root}/${EVAL_LOWER}_reasoning.log"
155
+ mkdir -p "${out_root}" "${shard_log_dir}"
156
+ local pids=()
157
+ for ((shard=0; shard<SHARD_COUNT; shard++)); do
158
+ local label
159
+ label="$(printf "biomni_react_%s_shard%02dof%02d" "${EVAL_LOWER}" "$((shard + 1))" "${SHARD_COUNT}")"
160
+ "${PYTHON_BIN}" "${BIOMNI_REACT_ROOT}/scripts/run_labbench_dbqa.py" \
161
+ --eval "${EVAL_NAME}" \
162
+ --split test \
163
+ --dev-size "${DEV_SIZE}" \
164
+ --test-size "${TEST_SIZE}" \
165
+ --seed "${SEED}" \
166
+ --shard-index "${shard}" \
167
+ --shard-count "${SHARD_COUNT}" \
168
+ --output-root "${out_root}" \
169
+ --result-file "${result_file}" \
170
+ --reasoning-log "${reasoning_log}" \
171
+ --skip-existing-results \
172
+ > "${shard_log_dir}/${label}.log" 2>&1 &
173
+ pids+=("$!")
174
+ wait_limited pids
175
+ done
176
+ for pid in "${pids[@]}"; do wait "${pid}" || true; done
177
+ echo "Biomni-ReAct done: ${out_root}"
178
+ }
179
+
180
+ run_react_code() {
181
+ local out_root="${REACT_CODE_ROOT}/results/cloningscenarios_${RUN_ID}"
182
+ local shard_log_dir="${out_root}/shard_logs"
183
+ local result_file="${out_root}/${EVAL_LOWER}_results.jsonl"
184
+ local reasoning_log="${out_root}/${EVAL_LOWER}_reasoning.log"
185
+ mkdir -p "${out_root}" "${shard_log_dir}"
186
+ local pids=()
187
+ for ((shard=0; shard<SHARD_COUNT; shard++)); do
188
+ local label
189
+ label="$(printf "react_code_%s_shard%02dof%02d" "${EVAL_LOWER}" "$((shard + 1))" "${SHARD_COUNT}")"
190
+ "${PYTHON_BIN}" "${REACT_CODE_ROOT}/run_labbench_react_code.py" \
191
+ --eval "${EVAL_NAME}" \
192
+ --split test \
193
+ --dev-size "${DEV_SIZE}" \
194
+ --test-size "${TEST_SIZE}" \
195
+ --seed "${SEED}" \
196
+ --shard-index "${shard}" \
197
+ --shard-count "${SHARD_COUNT}" \
198
+ --output-root "${out_root}" \
199
+ --result-file "${result_file}" \
200
+ --reasoning-log "${reasoning_log}" \
201
+ --skip-existing-results \
202
+ > "${shard_log_dir}/${label}.log" 2>&1 &
203
+ pids+=("$!")
204
+ wait_limited pids
205
+ done
206
+ for pid in "${pids[@]}"; do wait "${pid}" || true; done
207
+ echo "React+code done: ${out_root}"
208
+ }
209
+
210
+ echo "RUN_ID=${RUN_ID}"
211
+ echo "Logs: ${LOG_ROOT}"
212
+
213
+ run_biomanus > "${LOG_ROOT}/biomanus.log" 2>&1 &
214
+ pid_biomanus=$!
215
+ run_biomni_scales > "${LOG_ROOT}/biomni_scales.log" 2>&1 &
216
+ pid_biomni=$!
217
+ run_biomni_react > "${LOG_ROOT}/biomni_react.log" 2>&1 &
218
+ pid_biomni_react=$!
219
+ run_react_code > "${LOG_ROOT}/react_code.log" 2>&1 &
220
+ pid_react_code=$!
221
+
222
+ echo "Biomanus PID: ${pid_biomanus}"
223
+ echo "Biomni scales PID: ${pid_biomni}"
224
+ echo "Biomni-ReAct PID: ${pid_biomni_react}"
225
+ echo "React+code PID: ${pid_react_code}"
226
+
227
+ wait "${pid_biomanus}" || true
228
+ wait "${pid_biomni}" || true
229
+ wait "${pid_biomni_react}" || true
230
+ wait "${pid_react_code}" || true
231
+
232
+ echo "All CloningScenarios jobs finished for RUN_ID=${RUN_ID}"
run_cloningscenarios_reactcode_and_base_50shards.sh ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -u
3
+
4
+ RUN_ID="${RUN_ID:-cloning_50shards_$(date -u +%Y%m%d_%H%M%S)}"
5
+ PROJECT_ROOT="/225040511/project"
6
+ PYTHON_BIN="${LAB_BENCH_RUNNER_PYTHON:-/225040511/miniconda3/envs/biomni_e1/bin/python}"
7
+ LOG_ROOT="${PROJECT_ROOT}/cloningscenarios_logs/${RUN_ID}"
8
+ SHARD_COUNT="${CLONING_SHARD_COUNT:-50}"
9
+ SHARD_CONCURRENCY="${CLONING_SHARD_CONCURRENCY:-5}"
10
+ DEV_SIZE="${CLONING_DEV_SIZE:-0}"
11
+ TEST_SIZE="${CLONING_TEST_SIZE:-33}"
12
+ SEED="${CLONING_SEED:-20260514}"
13
+
14
+ mkdir -p "${LOG_ROOT}"
15
+
16
+ for ENV_FILE in "${PROJECT_ROOT}/.env" "${PROJECT_ROOT}/react_code_bioagent_deepseek/.env" "${PROJECT_ROOT}/LAB-Bench/.env"; do
17
+ if [[ -f "${ENV_FILE}" ]]; then
18
+ set -a
19
+ # shellcheck disable=SC1090
20
+ source "${ENV_FILE}"
21
+ set +a
22
+ fi
23
+ done
24
+
25
+ export DEEPSEEK_BASE_URL="${DEEPSEEK_BASE_URL:-https://api.deepseek.com/v1}"
26
+ export DEEPSEEK_MODEL_NAME="${DEEPSEEK_MODEL_NAME:-deepseek-chat}"
27
+ export REACT_CODE_API_KEY="${REACT_CODE_API_KEY:-${DEEPSEEK_API_KEY:-${BIOMNI_CUSTOM_API_KEY:-}}}"
28
+ export REACT_CODE_BASE_URL="${REACT_CODE_BASE_URL:-${DEEPSEEK_BASE_URL}}"
29
+ export REACT_CODE_MODEL="${REACT_CODE_MODEL:-${DEEPSEEK_MODEL_NAME}}"
30
+
31
+ wait_limited() {
32
+ local -n pid_array="$1"
33
+ if [[ "${#pid_array[@]}" -lt "${SHARD_CONCURRENCY}" ]]; then
34
+ return
35
+ fi
36
+ local pid="${pid_array[0]}"
37
+ wait "${pid}" || true
38
+ pid_array=("${pid_array[@]:1}")
39
+ }
40
+
41
+ count_rows() {
42
+ local file="$1"
43
+ if [[ -f "${file}" ]]; then
44
+ wc -l < "${file}"
45
+ else
46
+ echo 0
47
+ fi
48
+ }
49
+
50
+ run_react_code_bioagent() {
51
+ local runner="${PROJECT_ROOT}/React+code_labbench/run_labbench_react_code.py"
52
+ local out="${PROJECT_ROOT}/react_code_bioagent_deepseek/labbench_runs/react_code_cloningscenarios_${RUN_ID}"
53
+ local result="${out}/cloningscenarios_results.jsonl"
54
+ local reasoning="${out}/cloningscenarios_reasoning.log"
55
+ mkdir -p "${out}/shard_logs"
56
+ touch "${result}" "${reasoning}"
57
+ echo "react_code_bioagent output=${out}"
58
+ echo "react_code_bioagent before_rows=$(count_rows "${result}")"
59
+ local pids=()
60
+ for ((shard=0; shard<SHARD_COUNT; shard++)); do
61
+ local label
62
+ label="$(printf "react_code_bioagent_cloningscenarios_shard%02dof%02d" "$((shard + 1))" "${SHARD_COUNT}")"
63
+ "${PYTHON_BIN}" "${runner}" \
64
+ --eval CloningScenarios \
65
+ --split test \
66
+ --dev-size "${DEV_SIZE}" \
67
+ --test-size "${TEST_SIZE}" \
68
+ --seed "${SEED}" \
69
+ --shard-index "${shard}" \
70
+ --shard-count "${SHARD_COUNT}" \
71
+ --output-root "${out}" \
72
+ --result-file "${result}" \
73
+ --reasoning-log "${reasoning}" \
74
+ --skip-existing-results \
75
+ > "${out}/shard_logs/${label}.log" 2>&1 &
76
+ pids+=("$!")
77
+ wait_limited pids
78
+ done
79
+ for pid in "${pids[@]}"; do wait "${pid}" || true; done
80
+ echo "react_code_bioagent after_rows=$(count_rows "${result}")"
81
+ }
82
+
83
+ run_base_deepseek() {
84
+ local runner="${PROJECT_ROOT}/react_code_bioagent_deepseek/react_code_bioagent/labbench_runner.py"
85
+ local out="${PROJECT_ROOT}/base_llm_labbench/results/cloningscenarios_base_deepseek_${RUN_ID}"
86
+ local result="${out}/cloningscenarios_results.jsonl"
87
+ local reasoning="${out}/cloningscenarios_reasoning.log"
88
+ mkdir -p "${out}/shard_logs"
89
+ touch "${result}" "${reasoning}"
90
+ echo "base_deepseek output=${out}"
91
+ echo "base_deepseek before_rows=$(count_rows "${result}")"
92
+ local pids=()
93
+ for ((shard=0; shard<SHARD_COUNT; shard++)); do
94
+ local label
95
+ label="$(printf "base_deepseek_cloningscenarios_shard%02dof%02d" "$((shard + 1))" "${SHARD_COUNT}")"
96
+ "${PYTHON_BIN}" "${runner}" \
97
+ --evals CloningScenarios \
98
+ --splits test \
99
+ --dev-size "${DEV_SIZE}" \
100
+ --test-size "${TEST_SIZE}" \
101
+ --seed "${SEED}" \
102
+ --n-threads 1 \
103
+ --shard-index "${shard}" \
104
+ --shard-count "${SHARD_COUNT}" \
105
+ --output-root "${out}" \
106
+ --compact-results-path "${result}" \
107
+ --reasoning-log-path "${reasoning}" \
108
+ --skip-existing-results \
109
+ > "${out}/shard_logs/${label}.log" 2>&1 &
110
+ pids+=("$!")
111
+ wait_limited pids
112
+ done
113
+ for pid in "${pids[@]}"; do wait "${pid}" || true; done
114
+ echo "base_deepseek after_rows=$(count_rows "${result}")"
115
+ }
116
+
117
+ echo "RUN_ID=${RUN_ID}"
118
+ echo "SHARD_COUNT=${SHARD_COUNT}"
119
+ echo "SHARD_CONCURRENCY=${SHARD_CONCURRENCY}"
120
+
121
+ run_react_code_bioagent > "${LOG_ROOT}/react_code_bioagent_deepseek.log" 2>&1 &
122
+ pid_react=$!
123
+ run_base_deepseek > "${LOG_ROOT}/base_deepseek.log" 2>&1 &
124
+ pid_base=$!
125
+
126
+ echo "react_code_bioagent_pid=${pid_react}"
127
+ echo "base_deepseek_pid=${pid_base}"
128
+ echo "log_root=${LOG_ROOT}"
129
+
130
+ wait "${pid_react}" || true
131
+ wait "${pid_base}" || true
132
+ echo "finished RUN_ID=${RUN_ID}"