czty commited on
Commit
b2c86fd
·
verified ·
1 Parent(s): 33bf87a

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Biomanus_upload/.env.example +22 -0
  2. Biomanus_upload/.gitattributes +2 -0
  3. Biomanus_upload/.gitignore +236 -0
  4. Biomanus_upload/.pre-commit-config.yaml +41 -0
  5. Biomanus_upload/LICENSE +201 -0
  6. Biomanus_upload/README.md +322 -0
  7. Biomanus_upload/biomni/__init__.py +3 -0
  8. Biomanus_upload/biomni/agent/__init__.py +1 -0
  9. Biomanus_upload/biomni/agent/a1.py +0 -0
  10. Biomanus_upload/biomni/agent/env_collection.py +313 -0
  11. Biomanus_upload/biomni/agent/function_generator.py +119 -0
  12. Biomanus_upload/biomni/agent/qa_llm.py +50 -0
  13. Biomanus_upload/biomni/agent/react.py +465 -0
  14. Biomanus_upload/biomni/biorxiv_scripts/extract_biorxiv_tasks.py +404 -0
  15. Biomanus_upload/biomni/biorxiv_scripts/generate_function.py +72 -0
  16. Biomanus_upload/biomni/biorxiv_scripts/process_all_subjects.py +315 -0
  17. Biomanus_upload/biomni/config.py +119 -0
  18. Biomanus_upload/biomni/env_desc.py +221 -0
  19. Biomanus_upload/biomni/env_desc_cm.py +219 -0
  20. Biomanus_upload/biomni/eval/__init__.py +3 -0
  21. Biomanus_upload/biomni/eval/biomni_eval1.py +324 -0
  22. Biomanus_upload/biomni/graph/__init__.py +17 -0
  23. Biomanus_upload/biomni/graph/graph_rag.py +306 -0
  24. Biomanus_upload/biomni/graph/graph_router.py +674 -0
  25. Biomanus_upload/biomni/graph/planning_agent.py +1046 -0
  26. Biomanus_upload/biomni/graph/schema_extractor.py +1180 -0
  27. Biomanus_upload/biomni/graph/tool_graph.py +273 -0
  28. Biomanus_upload/biomni/graph/workflow_priors.py +147 -0
  29. Biomanus_upload/biomni/know_how/__init__.py +9 -0
  30. Biomanus_upload/biomni/know_how/loader.py +348 -0
  31. Biomanus_upload/biomni/know_how/resource/CRISPick_download_links.txt +238 -0
  32. Biomanus_upload/biomni/know_how/resource/addgene_grna_sequences.csv +322 -0
  33. Biomanus_upload/biomni/know_how/sgRNA_design_guide.md +419 -0
  34. Biomanus_upload/biomni/know_how/single_cell_annotation.md +223 -0
  35. Biomanus_upload/biomni/llm.py +276 -0
  36. Biomanus_upload/biomni/model/__init__.py +0 -0
  37. Biomanus_upload/biomni/model/query_rewriter.py +399 -0
  38. Biomanus_upload/biomni/model/retriever.py +502 -0
  39. Biomanus_upload/biomni/task/__init__.py +0 -0
  40. Biomanus_upload/biomni/task/base_task.py +18 -0
  41. Biomanus_upload/biomni/task/hle.py +146 -0
  42. Biomanus_upload/biomni/task/lab_bench.py +115 -0
  43. Biomanus_upload/biomni/tool/__init__.py +4 -0
  44. Biomanus_upload/biomni/tool/biochemistry.py +1026 -0
  45. Biomanus_upload/biomni/tool/bioengineering.py +1387 -0
  46. Biomanus_upload/biomni/tool/bioimaging.py +1394 -0
  47. Biomanus_upload/biomni/tool/biophysics.py +513 -0
  48. Biomanus_upload/biomni/tool/cancer_biology.py +1292 -0
  49. Biomanus_upload/biomni/tool/cell_biology.py +742 -0
  50. Biomanus_upload/biomni/tool/database.py +0 -0
Biomanus_upload/.env.example ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Biomni Environment Configuration
2
+ # Copy this file to .env and fill in your actual API keys
3
+
4
+ # Required: Anthropic API Key for Claude models
5
+ ANTHROPIC_API_KEY=your_anthropic_api_key_here
6
+
7
+ # Optional: OpenAI API Key (if using OpenAI models)
8
+ OPENAI_API_KEY=your_openai_api_key_here
9
+
10
+ # Optional: AWS Bedrock Configuration (if using AWS Bedrock models)
11
+ AWS_BEARER_TOKEN_BEDROCK=your_bedrock_api_key_here
12
+ AWS_REGION=us-east-1
13
+
14
+ # Optional: Custom model serving configuration
15
+ # CUSTOM_MODEL_BASE_URL=http://localhost:8000/v1
16
+ # CUSTOM_MODEL_API_KEY=your_custom_api_key_here
17
+
18
+ # Optional: Biomni data path (defaults to ./data)
19
+ # BIOMNI_DATA_PATH=/path/to/your/data
20
+
21
+ # Optional: Timeout settings
22
+ # BIOMNI_TIMEOUT_SECONDS=600
Biomanus_upload/.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.bam filter=lfs diff=lfs merge=lfs -text
2
+ *.h5ad filter=lfs diff=lfs merge=lfs -text
Biomanus_upload/.gitignore ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .gitignore
2
+ .buildplan
3
+ .env
4
+ debug*
5
+ .*
6
+ # Byte-compiled / optimized / DLL files
7
+ biomni_release/biomni_env/biomni_tools/
8
+ open_source_process.ipynb
9
+ biomni_env/biomni_tools/*
10
+ .gradio/
11
+ *xlsx
12
+ scripts/*.png
13
+ notebooks/figures/*
14
+ notebooks/tmp_dir/*
15
+ notebooks/tmp_directory/*
16
+ notebooks/save_folder/*
17
+ notebooks/cache
18
+ notebooks/\[draft*
19
+ notebooks/\[scratch*
20
+ notebooks/labbench/*
21
+ scripts/save_folder/*
22
+ scripts/\[draft*
23
+ scripts/tmp_dir/*
24
+ scripts/tmp_directory/*
25
+ scripts/figures/*
26
+ */scripts/outputs/*
27
+ */scripts/wandb/*
28
+ */results/*
29
+ bioagentos/agent/popper_agent.py
30
+ bioagentos/scripts/biorxiv_results
31
+ bioagentos/tool/data_tool/old_data_tool/*
32
+ examples/*.png
33
+ examples/*.csv
34
+ examples/*.fa
35
+ scratch/
36
+ packages/
37
+ data
38
+ dataset
39
+ logs
40
+ scripts/gradio
41
+ data_lake
42
+ __pycache__/
43
+ *.py[cod]
44
+ *$py.class
45
+ res/
46
+ examples/tmp*
47
+ bioagentos/task/lab_bench.py
48
+ *database/
49
+ .history/
50
+
51
+ temp.*
52
+
53
+ scripts/analysis/all_tools.txt
54
+
55
+ BIOMNI_GRADIO/biomni/tool/files/*
56
+
57
+
58
+ # C extensions
59
+ *.so
60
+
61
+ # Distribution / packaging
62
+ .Python
63
+ build/
64
+ develop-eggs/
65
+ dist/
66
+ downloads/
67
+ eggs/
68
+ .eggs/
69
+ lib/
70
+ lib64/
71
+ parts/
72
+ sdist/
73
+ var/
74
+ wheels/
75
+ share/python-wheels/
76
+ *.egg-info/
77
+ .installed.cfg
78
+ *.egg
79
+ MANIFEST
80
+
81
+ # PyInstaller
82
+ # Usually these files are written by a python script from a template
83
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
84
+ *.manifest
85
+ *.spec
86
+
87
+ # Installer logs
88
+ pip-log.txt
89
+ pip-delete-this-directory.txt
90
+
91
+ # Unit test / coverage reports
92
+ htmlcov/
93
+ .tox/
94
+ .nox/
95
+ .coverage
96
+ .coverage.*
97
+ .cache
98
+ nosetests.xml
99
+ coverage.xml
100
+ *.cover
101
+ *.py,cover
102
+ .hypothesis/
103
+ .pytest_cache/
104
+ cover/
105
+ test.ipynb
106
+
107
+ # Translations
108
+ *.mo
109
+ *.pot
110
+
111
+ # Django stuff:
112
+ *.log
113
+ local_settings.py
114
+ db.sqlite3
115
+ db.sqlite3-journal
116
+
117
+ # Flask stuff:
118
+ instance/
119
+ .webassets-cache
120
+
121
+ # Scrapy stuff:
122
+ .scrapy
123
+
124
+ # Sphinx documentation
125
+ docs/_build/
126
+
127
+ # PyBuilder
128
+ .pybuilder/
129
+ target/
130
+
131
+ # Jupyter Notebook
132
+ .ipynb_checkpoints
133
+
134
+ # IPython
135
+ profile_default/
136
+ ipython_config.py
137
+
138
+ # pyenv
139
+ # For a library or package, you might want to ignore these files since the code is
140
+ # intended to run in multiple environments; otherwise, check them in:
141
+ # .python-version
142
+
143
+ # pipenv
144
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
145
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
146
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
147
+ # install all needed dependencies.
148
+ #Pipfile.lock
149
+
150
+ # poetry
151
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
152
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
153
+ # commonly ignored for libraries.
154
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
155
+ #poetry.lock
156
+
157
+ # pdm
158
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
159
+ #pdm.lock
160
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
161
+ # in version control.
162
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
163
+ .pdm.toml
164
+ .pdm-python
165
+ .pdm-build/
166
+
167
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
168
+ __pypackages__/
169
+
170
+ # Celery stuff
171
+ celerybeat-schedule
172
+ celerybeat.pid
173
+
174
+ # SageMath parsed files
175
+ *.sage.py
176
+
177
+ # Environments
178
+ .env
179
+ .venv
180
+ env/
181
+ venv/
182
+ ENV/
183
+ env.bak/
184
+ venv.bak/
185
+ .envrc
186
+ uv.lock
187
+
188
+ # Spyder project settings
189
+ .spyderproject
190
+ .spyproject
191
+
192
+ # Rope project settings
193
+ .ropeproject
194
+
195
+ # mkdocs documentation
196
+ /site
197
+
198
+ # mypy
199
+ .mypy_cache/
200
+ .dmypy.json
201
+ dmypy.json
202
+
203
+ *.jsonl
204
+
205
+ # Pyre type checker
206
+ .pyre/
207
+
208
+ # pytype static type analyzer
209
+ .pytype/
210
+
211
+ # Cython debug symbols
212
+ cython_debug/
213
+
214
+ # macOS system files
215
+ .DS_Store
216
+ **/.DS_Store
217
+
218
+ # DDInter data files
219
+ biomni/tool/schema_db/ddinter_*.pkl
220
+ data/ddinter_raw/
221
+
222
+ # PyCharm
223
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
224
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
225
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
226
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
227
+ #.idea/
228
+
229
+ # VSCode
230
+ /.vscode
231
+
232
+ # Sphinx build
233
+ /docs/build/
234
+ /docs/source/api/
235
+ /paper_reproduction_runs/
236
+ /bioagent-bench-runs/
Biomanus_upload/.pre-commit-config.yaml ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fail_fast: false
2
+ default_language_version:
3
+ python: python3
4
+ default_stages:
5
+ - pre-commit
6
+ - pre-push
7
+ minimum_pre_commit_version: 2.16.0
8
+ repos:
9
+ - repo: https://github.com/biomejs/pre-commit
10
+ rev: v2.3.10
11
+ hooks:
12
+ - id: biome-format
13
+ - repo: https://github.com/astral-sh/ruff-pre-commit
14
+ rev: v0.14.11
15
+ hooks:
16
+ - id: ruff-check
17
+ args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes]
18
+ - id: ruff-format
19
+ - repo: https://github.com/pre-commit/pre-commit-hooks
20
+ rev: v6.0.0
21
+ hooks:
22
+ - id: detect-private-key
23
+ - id: check-ast
24
+ - id: end-of-file-fixer
25
+ - id: mixed-line-ending
26
+ args: [--fix=lf]
27
+ - id: trailing-whitespace
28
+ - id: check-case-conflict
29
+ - id: check-added-large-files
30
+ - id: check-toml
31
+ - id: check-yaml
32
+ - id: check-merge-conflict
33
+ - id: no-commit-to-branch
34
+ args: ["--branch=main"]
35
+ #- repo: https://github.com/pre-commit/mirrors-mypy
36
+ # rev: v1.16.1
37
+ # hooks:
38
+ # - id: mypy
39
+ # args: [--no-strict-optional, --ignore-missing-imports]
40
+ # additional_dependencies:
41
+ # ["types-setuptools", "types-requests", "types-attrs"]
Biomanus_upload/LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
Biomanus_upload/README.md ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Biomanus
2
+
3
+ Biomanus is a graph-guided extension of the Biomni A1 biomedical agent. It adds MCP server generation, MCP ToolGraph construction, GraphRAG-style tool routing, dynamic MCP registration, and benchmark runners for BioAgent-Bench and LAB-Bench.
4
+
5
+ This README explains how to install the environment, prepare MCP servers, build the MCP graph, and run the experiments.
6
+
7
+ ## 1. Repository Layout
8
+
9
+ ```text
10
+ biomni/ Core agent, tools, graph router, retriever, and utilities
11
+ biomni_env/ Conda environment files and setup scripts
12
+ demo_web/backend/tool2mcp.py Utility for converting bioinformatics tools into MCP servers
13
+ build_generated_mcp_graph.py Build MCP server catalogs and graph artifacts
14
+ run_bioagent_bench.py BioAgent-Bench runner
15
+ evaluate_bioagent_bench.py BioAgent-Bench evaluator
16
+ experiments/lab_bench/scripts/ LAB-Bench runners and graph utilities
17
+ graph_outputs/ Example prebuilt graph artifacts
18
+ ```
19
+
20
+ Large runtime artifacts, downloaded data lakes, benchmark outputs, node modules, and Git history are intentionally not included in this upload package.
21
+
22
+ ## 2. Install the Environment
23
+
24
+ Create and activate the Biomni environment:
25
+
26
+ ```bash
27
+ cd /path/to/Biomanus
28
+ conda env create -f biomni_env/environment.yml
29
+ conda activate biomni_e1
30
+ ```
31
+
32
+ For a smaller environment, use:
33
+
34
+ ```bash
35
+ conda env create -f biomni_env/fixed_env.yml
36
+ conda activate biomni_e1
37
+ ```
38
+
39
+ Install extra packages used by MCP generation and benchmark execution:
40
+
41
+ ```bash
42
+ pip install fastmcp mcp openai anthropic python-dotenv pymupdf pipreqs
43
+ pip install langchain langchain-openai langchain-anthropic langgraph
44
+ ```
45
+
46
+ Because this upload package is source-first and does not require a wheel install, run commands from the repository root or export:
47
+
48
+ ```bash
49
+ export PYTHONPATH="$PWD:$PYTHONPATH"
50
+ export BIOMNI_EXECUTION_ENV_PREFIX="$CONDA_PREFIX"
51
+ export BIOMNI_EXECUTION_PYTHON="$CONDA_PREFIX/bin/python"
52
+ ```
53
+
54
+ If your experiments need command-line bioinformatics tools, install them into the same conda environment. The full upstream Biomni setup script is available at:
55
+
56
+ ```bash
57
+ bash biomni_env/setup.sh
58
+ ```
59
+
60
+ This full setup can take many hours and requires substantial disk space.
61
+
62
+ ## 3. Configure the LLM Backend
63
+
64
+ Biomanus uses Biomni's `Custom` OpenAI-compatible backend in the main experiments. For DeepSeek:
65
+
66
+ ```bash
67
+ export DEEPSEEK_API_KEY="your_key"
68
+ export DEEPSEEK_BASE_URL="https://api.deepseek.com/v1"
69
+ export DEEPSEEK_MODEL_NAME="deepseek-chat"
70
+
71
+ export BIOMNI_SOURCE="Custom"
72
+ export BIOMNI_LLM="$DEEPSEEK_MODEL_NAME"
73
+ export BIOMNI_CUSTOM_BASE_URL="$DEEPSEEK_BASE_URL"
74
+ export BIOMNI_CUSTOM_API_KEY="$DEEPSEEK_API_KEY"
75
+ export BIOMNI_LLM_PROVIDER="deepseek"
76
+ ```
77
+
78
+ For another OpenAI-compatible endpoint, keep `BIOMNI_SOURCE=Custom` and replace the base URL, model name, and API key.
79
+
80
+ ## 4. Prepare MCP Servers
81
+
82
+ Graph-based execution requires local MCP server directories. Each generated server should follow this structure:
83
+
84
+ ```text
85
+ demo_web/backend/data/mcp_generated/
86
+ mcp_<server_name>/
87
+ app/
88
+ <server_name>_server.py
89
+ <server_name>_shim_server.py # optional but recommended
90
+ Dockerfile
91
+ docker-compose.yml
92
+ requirements.txt
93
+ environment.yaml
94
+ ```
95
+
96
+ Create the MCP directories:
97
+
98
+ ```bash
99
+ mkdir -p demo_web/backend/data/mcp_generated
100
+ mkdir -p demo_web/backend/data/merged_prefer_help_txt
101
+ ```
102
+
103
+ ### Option A: Copy Existing MCP Servers
104
+
105
+ If you already generated MCP servers elsewhere, copy the `mcp_*` directories into:
106
+
107
+ ```text
108
+ demo_web/backend/data/mcp_generated/
109
+ ```
110
+
111
+ Then patch generated servers so they expose stdio-compatible MCP entrypoints:
112
+
113
+ ```bash
114
+ python patch_raw_mcp_servers.py --mcp-root demo_web/backend/data/mcp_generated
115
+ python patch_generated_mcp_shims.py --mcp-root demo_web/backend/data/mcp_generated
116
+ ```
117
+
118
+ ### Option B: Generate One MCP Server from a Tool Manual
119
+
120
+ Use `tool2mcp.py` to convert a bioinformatics command-line tool and its help/manual text into an MCP server.
121
+
122
+ Example using a local help text:
123
+
124
+ ```bash
125
+ python demo_web/backend/tool2mcp.py \
126
+ --name kallisto \
127
+ --manual docs/kallisto_help.txt \
128
+ --output_location demo_web/backend/data/mcp_generated \
129
+ --llm_provider deepseek
130
+ ```
131
+
132
+ Example using the installed tool's help command:
133
+
134
+ ```bash
135
+ python demo_web/backend/tool2mcp.py \
136
+ --name fastqc \
137
+ --manual=--help \
138
+ --run_help_command True \
139
+ --output_location demo_web/backend/data/mcp_generated \
140
+ --llm_provider deepseek
141
+ ```
142
+
143
+ The converter calls an LLM, so the corresponding API key must be configured. Supported providers include `deepseek`, `openai`, `claude`, `azure`, and `gemini`.
144
+
145
+ ## 5. Build the MCP Graph
146
+
147
+ Once MCP servers exist under `demo_web/backend/data/mcp_generated`, build a graph over all servers:
148
+
149
+ ```bash
150
+ python build_generated_mcp_graph.py \
151
+ --mcp-root demo_web/backend/data/mcp_generated \
152
+ --help-root demo_web/backend/data/merged_prefer_help_txt \
153
+ --output-root graph_outputs \
154
+ --preset all \
155
+ --python-cmd "$BIOMNI_EXECUTION_PYTHON"
156
+ ```
157
+
158
+ The output directory will look like:
159
+
160
+ ```text
161
+ graph_outputs/mcp_generated_graph_all_<timestamp>/
162
+ server_catalog.json
163
+ graph_nodes.json
164
+ graph_edges.json
165
+ server_semantics.json
166
+ graph_summary.json
167
+ ```
168
+
169
+ For BioAgent-Bench, you can build a smaller graph containing high-signal benchmark servers:
170
+
171
+ ```bash
172
+ python build_generated_mcp_graph.py \
173
+ --mcp-root demo_web/backend/data/mcp_generated \
174
+ --help-root demo_web/backend/data/merged_prefer_help_txt \
175
+ --output-root graph_outputs \
176
+ --preset bioagent-bench \
177
+ --python-cmd "$BIOMNI_EXECUTION_PYTHON"
178
+ ```
179
+
180
+ This upload package includes an example graph under `graph_outputs/`. Use it for inspection or as a template. For executable experiments on a new machine, rebuild the graph after placing MCP servers locally so `server_catalog.json` contains valid local commands.
181
+
182
+ ## 6. Run BioAgent-Bench
183
+
184
+ First download or clone BioAgent-Bench and set the metadata/data paths:
185
+
186
+ ```bash
187
+ export BIOAGENT_BENCH_DATASET_ROOT="/path/to/bioagent-bench/dataset"
188
+ export BIOAGENT_BENCH_METADATA="/path/to/bioagent-bench/src/task_metadata.json"
189
+ ```
190
+
191
+ Run a single task:
192
+
193
+ ```bash
194
+ python run_bioagent_bench.py \
195
+ --task transcript-quant \
196
+ --dataset-root "$BIOAGENT_BENCH_DATASET_ROOT" \
197
+ --metadata "$BIOAGENT_BENCH_METADATA" \
198
+ --mcp-graph graph_outputs/mcp_generated_graph_benchmark_<timestamp> \
199
+ --executable-mcp-only \
200
+ --output-root experiments/bioagent-bench-runs \
201
+ --execution-env-prefix "$CONDA_PREFIX"
202
+ ```
203
+
204
+ Run all supported tasks:
205
+
206
+ ```bash
207
+ python run_bioagent_bench.py \
208
+ --all \
209
+ --dataset-root "$BIOAGENT_BENCH_DATASET_ROOT" \
210
+ --metadata "$BIOAGENT_BENCH_METADATA" \
211
+ --mcp-graph graph_outputs/mcp_generated_graph_benchmark_<timestamp> \
212
+ --executable-mcp-only \
213
+ --output-root experiments/bioagent-bench-runs \
214
+ --execution-env-prefix "$CONDA_PREFIX"
215
+ ```
216
+
217
+ Useful ablation switches:
218
+
219
+ ```bash
220
+ --no-graph-retriever # disable graph-guided routing
221
+ --no-mcp # disable MCP registration/execution
222
+ --no-use-tool-retriever # disable internal Biomni tool retrieval
223
+ ```
224
+
225
+ Evaluate BioAgent-Bench outputs:
226
+
227
+ ```bash
228
+ python evaluate_bioagent_bench.py \
229
+ --all \
230
+ --runs-root experiments/bioagent-bench-runs \
231
+ --dataset-root "$BIOAGENT_BENCH_DATASET_ROOT" \
232
+ --judge-mode rule \
233
+ --output experiments/bioagent-bench-runs/latest_evaluation.json
234
+ ```
235
+
236
+ For LLM-based judging, set the judge provider/key and use `--judge-mode llm` or `--judge-mode both`.
237
+
238
+ ## 7. Run LAB-Bench
239
+
240
+ First install or clone LAB-Bench and set:
241
+
242
+ ```bash
243
+ export LAB_BENCH_ROOT="/path/to/LAB-Bench"
244
+ ```
245
+
246
+ Run a small debug pass:
247
+
248
+ ```bash
249
+ python experiments/lab_bench/scripts/run_labbench_with_hypobioos.py \
250
+ --evals DbQA SeqQA \
251
+ --splits test \
252
+ --debug \
253
+ --graph-dir graph_outputs/mcp_generated_graph_all_<timestamp> \
254
+ --output-root experiments/lab_bench/results/debug \
255
+ --agent-root experiments/lab_bench/agent_runtime/debug \
256
+ --compact-results-path experiments/lab_bench/results/debug/results.jsonl \
257
+ --reasoning-log-path experiments/lab_bench/results/debug/reasoning.log \
258
+ --compact-output-only
259
+ ```
260
+
261
+ Run the main DbQA/SeqQA protocol:
262
+
263
+ ```bash
264
+ python experiments/lab_bench/scripts/run_labbench_with_hypobioos.py \
265
+ --evals DbQA SeqQA \
266
+ --splits test \
267
+ --dev-size 45 \
268
+ --test-size 315 \
269
+ --seed 20260514 \
270
+ --graph-dir graph_outputs/mcp_generated_graph_all_<timestamp> \
271
+ --mcp-server-top-k 8 \
272
+ --mcp-tool-top-k 12 \
273
+ --mcp-server-candidate-pool 24 \
274
+ --output-root experiments/lab_bench/results/main \
275
+ --agent-root experiments/lab_bench/agent_runtime/main \
276
+ --compact-results-path experiments/lab_bench/results/main/results.jsonl \
277
+ --reasoning-log-path experiments/lab_bench/results/main/reasoning.log \
278
+ --compact-output-only
279
+ ```
280
+
281
+ For parallel sharded runs, call the same runner repeatedly with `--shard-index` and `--shard-count`, writing all shards to the same compact JSONL with `--skip-existing-results`.
282
+
283
+ ## 8. Recommended Reproduction Order
284
+
285
+ 1. Create and activate the conda environment.
286
+ 2. Configure the LLM backend.
287
+ 3. Generate or copy MCP servers into `demo_web/backend/data/mcp_generated`.
288
+ 4. Patch MCP shims.
289
+ 5. Build the MCP graph.
290
+ 6. Run a single BioAgent-Bench task as a smoke test.
291
+ 7. Run LAB-Bench in `--debug` mode.
292
+ 8. Launch full benchmark runs and evaluate.
293
+
294
+ ## 9. Common Issues
295
+
296
+ `MCP root does not exist`
297
+
298
+ Create `demo_web/backend/data/mcp_generated` and copy or generate `mcp_*` server directories.
299
+
300
+ `Help root does not exist`
301
+
302
+ Create an empty help directory:
303
+
304
+ ```bash
305
+ mkdir -p demo_web/backend/data/merged_prefer_help_txt
306
+ ```
307
+
308
+ `server_catalog.json points to old paths`
309
+
310
+ Rebuild the graph on the current machine after placing MCP servers locally. The graph stores executable command paths.
311
+
312
+ `Missing API key`
313
+
314
+ Export `DEEPSEEK_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or the matching provider key before running agents or MCP generation.
315
+
316
+ `LAB-Bench import errors`
317
+
318
+ Set `LAB_BENCH_ROOT` to the local LAB-Bench checkout and install LAB-Bench dependencies in the active environment.
319
+
320
+ `BioAgent-Bench cannot find metadata`
321
+
322
+ Set `BIOAGENT_BENCH_METADATA` and `BIOAGENT_BENCH_DATASET_ROOT`, or pass `--metadata` and `--dataset-root` explicitly.
Biomanus_upload/biomni/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .version import __version__
2
+
3
+ __all__ = ["__version__"]
Biomanus_upload/biomni/agent/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from biomni.agent.a1 import A1 # noqa: F401
Biomanus_upload/biomni/agent/a1.py ADDED
The diff for this file is too large to render. See raw diff
 
Biomanus_upload/biomni/agent/env_collection.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ from typing import Any
5
+
6
+ from langchain_core.prompts import ChatPromptTemplate
7
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
8
+
9
+ from biomni.agent.base_agent import base_agent
10
+
11
+
12
+ class PaperTaskExtractor(base_agent):
13
+ """Agent that extracts generalizable tasks/experiments from academic papers.
14
+ It processes papers in chunks and identifies common research tasks that could be shared across papers.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ llm="claude-3-7-sonnet-20250219",
20
+ cheap_llm=None,
21
+ tools=None,
22
+ chunk_size=4000,
23
+ chunk_overlap=400,
24
+ ):
25
+ """Initialize the PaperTaskExtractor agent.
26
+
27
+ Args:
28
+ llm (str): The LLM model to use
29
+ cheap_llm (str, optional): A cheaper LLM for simpler tasks
30
+ tools (list, optional): Any tools to use (not needed for this agent)
31
+ chunk_size (int): Size of text chunks for processing
32
+ chunk_overlap (int): Overlap between chunks
33
+
34
+ """
35
+ super().__init__(llm, cheap_llm, tools)
36
+ self.chunk_size = chunk_size
37
+ self.chunk_overlap = chunk_overlap
38
+ self.log = []
39
+ self.configure()
40
+
41
+ def configure(self):
42
+ """Configure the agent with appropriate prompts."""
43
+ # Prompt for analyzing paper chunks
44
+ self.chunk_analysis_prompt = """You are a research methodology expert specializing in identifying computational tasks and data analysis procedures in academic papers.
45
+
46
+ Your job is to analyze chunks of academic papers and identify ONLY the most common, generalizable computational tasks that are widely used across biomedical research and can be implemented with Python or Linux code.
47
+
48
+ STRICT GUIDELINES:
49
+ 1. ONLY extract tasks that are extremely common and standard in computational biomedical research
50
+ 2. Each task MUST have clear, well-defined inputs and outputs
51
+ 3. Tasks MUST be generalizable across many different papers and research questions
52
+ 4. Be VERY selective - only include tasks that appear in hundreds of papers
53
+ 5. If a task is specific to this paper, unclear, or not widely used, DO NOT include it
54
+ 6. Focus on computational tasks that can be implemented with Python or Linux code
55
+ 7. Each task should be something that could be implemented as a function with clear inputs/outputs
56
+ 8. Also identify commonly used databases and software packages mentioned in the text
57
+ 9. Tasks MUST be CONCRETE and SPECIFIC - include exact methodological details
58
+ 10. Avoid vague task names like "Statistical Analysis" - instead use specific protocol names like "Two-way ANOVA with Tukey's Post-hoc Test using SciPy"
59
+ 11. DO NOT include wet lab procedures that cannot be implemented computationally
60
+ 12. ONLY include tasks that could be automated with code
61
+
62
+ For the following chunk of text from an academic paper, provide:
63
+ 1. A list of ONLY the most common, generalizable COMPUTATIONAL tasks identified (be extremely selective)
64
+ 2. For each task, clearly define:
65
+ - Task name: A SPECIFIC and CONCRETE name with methodological details (e.g., "RNA-seq Differential Expression Analysis with DESeq2" instead of just "Gene Expression Analysis")
66
+ - Input: What SPECIFIC data or parameters the task requires
67
+ - Output: What SPECIFIC data or results the task produces
68
+ - Code implementation: How this task could be implemented with Python or Linux code, including key libraries/packages
69
+ - Frequency: How common this computational task is in biomedical research
70
+ - Standard methods: The established computational techniques used to perform this task
71
+ - Example: A brief description of how THIS specific paper uses this task (with specific details from the paper)
72
+ 3. A list of commonly used databases mentioned in the text (if any)
73
+ 4. A list of commonly used software packages/tools mentioned in the text (if any)
74
+
75
+ PAPER CHUNK:
76
+ {chunk_text}
77
+
78
+ Remember, it's better to return NO tasks than to include tasks that aren't extremely common, generalizable, and implementable with code. Quality over quantity is essential. Tasks MUST be CONCRETE with SPECIFIC methodological details and MUST be implementable with Python or Linux code.
79
+ """
80
+
81
+ # Prompt for consolidating tasks
82
+ self.consolidation_prompt = """You are a research methodology expert. Your task is to consolidate lists of computational research tasks extracted from different chunks of an academic paper.
83
+
84
+ BE EXTREMELY SELECTIVE. Only include tasks that are:
85
+ 1. Fundamental to computational biomedical research
86
+ 2. Used in hundreds of papers across different subfields
87
+ 3. Have clear, well-defined inputs and outputs
88
+ 4. Represent standard computational approaches
89
+ 5. Could be implemented as a function with specific inputs and outputs
90
+ 6. Are CONCRETE and SPECIFIC with exact methodological details
91
+ 7. Can be implemented with Python or Linux code
92
+ 8. Are computational in nature, not wet lab procedures
93
+
94
+ REMOVE any tasks that:
95
+ - Are specific to a particular paper or dataset
96
+ - Lack clear inputs or outputs
97
+ - Are not widely used across biomedical research
98
+ - Are vague or poorly defined
99
+ - Represent niche or specialized techniques
100
+ - Have generic names without specific methodological details
101
+ - Cannot be implemented with code
102
+ - Require physical lab equipment or manual intervention
103
+
104
+ The output should be a JSON object with three main keys:
105
+ 1. "tasks": A list of task objects, where each task has:
106
+ - "task_name": A SPECIFIC and CONCRETE name with methodological details (e.g., "Single-cell RNA-seq Clustering with Seurat" instead of just "Cell Clustering")
107
+ - "description": A clear description of what the computational task does
108
+ - "inputs": SPECIFIC data types or parameters the task requires
109
+ - "outputs": SPECIFIC data types or results the task produces
110
+ - "code_implementation": How this task could be implemented with Python or Linux code, including key libraries/packages and a brief pseudocode example
111
+ - "frequency": How common this computational task is in biomedical research
112
+ - "standard_methods": The established computational techniques used to perform this task
113
+ - "example": A specific example from THIS paper showing how the task was used (with concrete details)
114
+
115
+ 2. "databases": A list of database objects, where each database has:
116
+ - "name": The name of the database
117
+ - "description": A brief description of what the database contains
118
+ - "url": The URL of the database (if mentioned)
119
+ - "usage": How the database is commonly used in computational biomedical research
120
+ - "example": How this specific paper uses the database
121
+
122
+ 3. "software": A list of software package objects, where each package has:
123
+ - "name": The name of the software package
124
+ - "description": A brief description of what the software does
125
+ - "url": The URL or reference to the software (if mentioned)
126
+ - "usage": How the software is commonly used in computational biomedical research
127
+ - "example": How this specific paper uses the software
128
+
129
+ EXTRACTED INFORMATION FROM PAPER CHUNKS:
130
+ {task_lists}
131
+
132
+ Be ruthless in filtering - it's better to return a few truly common computational tasks than many that aren't universal or implementable with code.
133
+ Respond with only a valid JSON object containing the three lists described above.
134
+ """
135
+
136
+ def process_paper(self, paper_text: str) -> dict[str, list[dict[str, Any]]]:
137
+ """Process a paper and extract generalizable tasks/experiments, databases, and software.
138
+
139
+ Args:
140
+ paper_text (str): The full text of the paper
141
+
142
+ Returns:
143
+ Dict[str, List[Dict[str, Any]]]: A dictionary with tasks, databases, and software
144
+
145
+ """
146
+ # Split the paper into chunks
147
+ text_splitter = RecursiveCharacterTextSplitter(
148
+ chunk_size=self.chunk_size,
149
+ chunk_overlap=self.chunk_overlap,
150
+ length_function=len,
151
+ separators=["\n\n", "\n", ". ", " ", ""],
152
+ )
153
+ chunks = text_splitter.split_text(paper_text)
154
+
155
+ # Process each chunk to extract tasks
156
+ chunk_results = []
157
+ for i, chunk in enumerate(chunks):
158
+ print(f"Processing chunk {i + 1}/{len(chunks)}...")
159
+ chunk_tasks = self._process_chunk(chunk)
160
+ chunk_results.append(chunk_tasks)
161
+
162
+ # Consolidate tasks from all chunks
163
+ consolidated_results = self._consolidate_tasks(chunk_results)
164
+ return consolidated_results
165
+
166
+ def _process_chunk(self, chunk_text: str) -> str:
167
+ """Process a single chunk of the paper to extract tasks.
168
+
169
+ Args:
170
+ chunk_text (str): The chunk text to process
171
+
172
+ Returns:
173
+ str: Extracted tasks from this chunk
174
+
175
+ """
176
+ prompt = self.chunk_analysis_prompt.format(chunk_text=chunk_text)
177
+ message = self.llm.invoke(prompt)
178
+ return message.content
179
+
180
+ def _consolidate_tasks(self, chunk_results: list[str]) -> dict[str, list[dict[str, Any]]]:
181
+ """Consolidate tasks, databases, and software extracted from different chunks into a unified structure.
182
+
183
+ Args:
184
+ chunk_results (List[str]): Results from each chunk
185
+
186
+ Returns:
187
+ Dict[str, List[Dict[str, Any]]]: Consolidated information with tasks, databases, and software
188
+
189
+ """
190
+ # Combine all chunk results
191
+ all_tasks = "\n\n===== CHUNK SEPARATOR =====\n\n".join(chunk_results)
192
+
193
+ # Use the consolidation prompt to merge and organize tasks
194
+ prompt = self.consolidation_prompt.format(task_lists=all_tasks)
195
+ response = self.llm.invoke(prompt)
196
+
197
+ # Extract the JSON from the response
198
+ try:
199
+ # Try to parse the entire response as JSON
200
+ result = json.loads(response.content)
201
+ except json.JSONDecodeError:
202
+ # If that fails, try to extract JSON from the text
203
+ try:
204
+ # Look for JSON-like content between triple backticks
205
+ json_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", response.content)
206
+ if json_match:
207
+ result = json.loads(json_match.group(1))
208
+ else:
209
+ # Fallback: just return the text response
210
+ return {
211
+ "tasks": [
212
+ {
213
+ "error": "Could not parse JSON",
214
+ "raw_response": response.content,
215
+ }
216
+ ],
217
+ "databases": [],
218
+ "software": [],
219
+ }
220
+ except Exception:
221
+ return {
222
+ "tasks": [
223
+ {
224
+ "error": "Could not parse JSON",
225
+ "raw_response": response.content,
226
+ }
227
+ ],
228
+ "databases": [],
229
+ "software": [],
230
+ }
231
+
232
+ # Ensure the result has the expected structure
233
+ if not isinstance(result, dict):
234
+ result = {
235
+ "tasks": result if isinstance(result, list) else [],
236
+ "databases": [],
237
+ "software": [],
238
+ }
239
+
240
+ # Ensure all required keys exist
241
+ for key in ["tasks", "databases", "software"]:
242
+ if key not in result:
243
+ result[key] = []
244
+
245
+ return result
246
+
247
+ def go(self, paper_text: str):
248
+ """Process a paper and return the extracted tasks, databases, and software.
249
+
250
+ Args:
251
+ paper_text (str): The full text of the paper
252
+
253
+ Returns:
254
+ tuple: (log, results) where log is a list of processing steps and results is the final result
255
+
256
+ """
257
+ self.log = []
258
+ self.log.append(
259
+ (
260
+ "user",
261
+ "Extract only the most common and generalizable biomedical research tasks, databases, and software from this paper",
262
+ )
263
+ )
264
+
265
+ results = self.process_paper(paper_text)
266
+
267
+ result_str = json.dumps(results, indent=2)
268
+ self.log.append(("assistant", result_str))
269
+
270
+ return self.log, results
271
+
272
+ def save_results(self, results: dict[str, list[dict[str, Any]]], output_path: str):
273
+ """Save the extracted tasks, databases, and software to a JSON file.
274
+
275
+ Args:
276
+ results (Dict[str, List[Dict[str, Any]]]): The extracted tasks, databases, and software
277
+ output_path (str): Path to save the results
278
+
279
+ """
280
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
281
+ with open(output_path, "w") as f:
282
+ json.dump(results, f, indent=2)
283
+ print(f"Results saved to {output_path}")
284
+
285
+ def result_formatting(self, output_class, task_intention):
286
+ """Format the results according to a specific output class.
287
+
288
+ Args:
289
+ output_class: The class to format the output as
290
+ task_intention: Description of the task
291
+
292
+ Returns:
293
+ The formatted result
294
+
295
+ """
296
+ format_check_prompt = ChatPromptTemplate.from_messages(
297
+ [
298
+ (
299
+ "system",
300
+ (
301
+ "You are evaluateGPT, tasked with extract and parse the task output based on the history of an agent. "
302
+ "Review the entire history of messages provided. "
303
+ "Here is the task output requirement: \n"
304
+ f"'{task_intention.replace('{', '{{').replace('}', '}}')}'.\n"
305
+ ),
306
+ ),
307
+ ("placeholder", "{messages}"),
308
+ ]
309
+ )
310
+
311
+ checker_llm = format_check_prompt | self.llm.with_structured_output(output_class)
312
+ result = checker_llm.invoke({"messages": [("user", str(self.log))]}).dict()
313
+ return result
Biomanus_upload/biomni/agent/function_generator.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ from biomni.llm import get_llm
4
+
5
+
6
+ class base_agent:
7
+ def __init__(self, llm="claude-3-haiku-20240307", cheap_llm=None, tools=None, temperature=0.7):
8
+ self.tools = tools
9
+ self.llm = get_llm(llm, temperature)
10
+ if cheap_llm is None:
11
+ self.cheap_llm = llm
12
+ else:
13
+ self.cheap_llm = cheap_llm
14
+
15
+ def configure(self):
16
+ pass
17
+
18
+ def go(self, input):
19
+ pass
20
+
21
+
22
+ class FunctionGenerator(base_agent):
23
+ """Agent that generates executable Python code scripts given a task description."""
24
+
25
+ def __init__(self, llm="claude-3-7-sonnet-20250219", cheap_llm=None, temperature=0.7):
26
+ """Initialize the PaperTaskExtractor agent.
27
+
28
+ Args:
29
+ llm (str): The LLM model to use
30
+ cheap_llm (str, optional): A cheaper LLM for simpler tasks
31
+ """
32
+ super().__init__(llm, cheap_llm, temperature)
33
+ self.log = []
34
+ self.configure()
35
+
36
+ def configure(self):
37
+ """Configure the agent with appropriate prompts."""
38
+ # Prompt for Python code generation
39
+ self.system_prompt = """You are a senior Python engineer. Generate robust, idiomatic Python code that solves the user's task. Requirements:
40
+ 1. Output ONLY Python code, ideally inside a single triple-backtick code block.
41
+ 2. Include minimal inline comments and a small docstring.
42
+ 3. Add a `main()` and an `if __name__ == '__main__':` guard when appropriate.
43
+ 4. Avoid external dependencies unless necessary; if used, show `pip` installs in comments.
44
+ 5. Do not include prose before or after the code.
45
+ 6. When applicable, prioritize the use of codes on public repositories, such as HuggingFace or Github
46
+
47
+ Generate Python codes for the following task:
48
+ {task}
49
+ """
50
+
51
+ def _generate_code(self, task_description: str) -> str:
52
+ """Generate codes given a task description.
53
+ Args:
54
+ task_description (str): task descriptions (possibly generated from previous steps)
55
+
56
+ Returns:
57
+ str: generated code string
58
+
59
+ """
60
+ prompt = self.system_prompt.format(task=task_description)
61
+ message = self.llm.invoke(prompt)
62
+ return message.content
63
+
64
+ def _generate_script_filename(self, task_description: str, max_words: int = 6) -> str:
65
+ """
66
+ Generate a safe, meaningful Python script filename from a task description.
67
+ Poised for update: may ask the agent to suggest meaningful names.
68
+
69
+ Parameters:
70
+ -----------
71
+ task_description (str): task descriptions (possibly generated from previous steps)
72
+
73
+ max_words : int
74
+ Maximum number of words to include in the filename.
75
+
76
+ Returns:
77
+ --------
78
+ str
79
+ A lowercase, hyphen-free, safe filename ending in '.py'.
80
+ """
81
+ # Lowercase and remove non-alphanumeric (allow spaces for splitting)
82
+ cleaned = re.sub(r"[^a-zA-Z0-9\s]", "", task_description.lower())
83
+
84
+ # Tokenize and select top words
85
+ words = cleaned.split()
86
+ selected_words = words[:max_words] if words else ["script"]
87
+
88
+ # Join with underscores
89
+ base_name = "_".join(selected_words)
90
+ return f"{base_name}.py"
91
+
92
+ def go(self, task_description: str):
93
+ """Implement the inherited function to get the tasks done.
94
+
95
+ Args:
96
+ task_description (str): task descriptions (possibly generated from previous steps)
97
+
98
+ Returns:
99
+ tuple: (script_filename, results) where script_filename is a generated name for script file and results is the generated codes
100
+
101
+ """
102
+ self.log = []
103
+ self.log.append(
104
+ (
105
+ "user",
106
+ "Generate Python codes given a task description",
107
+ )
108
+ )
109
+
110
+ script_filename = self._generate_script_filename(task_description)
111
+ results = self._generate_code(task_description)
112
+ return script_filename, self._extract_code_block(results)
113
+
114
+ def _extract_code_block(self, s: str) -> str:
115
+ """
116
+ Extract the first fenced code block (``` or ```python) from s.
117
+ """
118
+ m = re.search(r"```(?:python)?\s*(.+?)\s*```", s, flags=re.DOTALL | re.IGNORECASE)
119
+ return m.group(1).strip() if m else None
Biomanus_upload/biomni/agent/qa_llm.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.prompts import ChatPromptTemplate
2
+
3
+ from biomni.llm import get_llm
4
+
5
+
6
+ class qa_llm:
7
+ def __init__(self, path="./data", llm="claude-3-haiku-20240307", lab_bench_reproduce=False):
8
+ self.path = path
9
+ self.llm = get_llm(llm)
10
+
11
+ if lab_bench_reproduce:
12
+ self.prompt_modifier = """
13
+ The following is a multiple choice question about biology.
14
+ Please answer by responding with the letter of the correct answer.
15
+
16
+ Think step by step. \n
17
+ """
18
+ else:
19
+ self.prompt_modifier = ""
20
+ self.log = []
21
+
22
+ def configure(self):
23
+ pass
24
+
25
+ def go(self, input):
26
+ self.log = []
27
+ self.log.append(("user", input))
28
+ message = self.llm.invoke(self.prompt_modifier + input)
29
+ self.log.append(("assistant", message.content))
30
+ return [message.content], message.content
31
+
32
+ def result_formatting(self, output_class, task_intention):
33
+ self.format_check_prompt = ChatPromptTemplate.from_messages(
34
+ [
35
+ (
36
+ "system",
37
+ (
38
+ "You are evaluateGPT, tasked with extract and parse the task output based on the history of an agent. "
39
+ "Review the entire history of messages provided. "
40
+ "Here is the task output requirement: \n"
41
+ f"'{task_intention.replace('{', '{{').replace('}', '}}')}'.\n"
42
+ ),
43
+ ),
44
+ ("placeholder", "{messages}"),
45
+ ]
46
+ )
47
+
48
+ checker_llm = self.format_check_prompt | self.llm.with_structured_output(output_class)
49
+ result = checker_llm.invoke({"messages": [("user", str(self.log))]}).dict()
50
+ return result
Biomanus_upload/biomni/agent/react.py ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import glob
2
+ import inspect
3
+ import json
4
+ import os
5
+ import signal
6
+ from collections.abc import Sequence
7
+ from functools import wraps
8
+ from multiprocessing import Process, Queue
9
+ from typing import Annotated, TypedDict
10
+
11
+ from langchain_core.messages import BaseMessage, SystemMessage, ToolMessage
12
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
13
+ from langchain_core.runnables import RunnableConfig
14
+ from langgraph.graph import END, StateGraph
15
+ from langgraph.graph.message import add_messages
16
+
17
+ from biomni.config import default_config
18
+ from biomni.env_desc import data_lake_dict, library_content_dict
19
+ from biomni.llm import get_llm
20
+ from biomni.model.retriever import ToolRetriever
21
+ from biomni.tool.tool_registry import ToolRegistry
22
+ from biomni.utils import (
23
+ api_schema_to_langchain_tool,
24
+ function_to_api_schema,
25
+ pretty_print,
26
+ read_module2api,
27
+ )
28
+
29
+
30
+ # Define the AgentState TypedDict for our custom implementation
31
+ class AgentState(TypedDict):
32
+ """The state of the agent."""
33
+
34
+ # add_messages is a reducer that combines message sequences
35
+ messages: Annotated[Sequence[BaseMessage], add_messages]
36
+
37
+
38
+ class react:
39
+ def __init__(
40
+ self,
41
+ path: str | None = None,
42
+ llm: str | None = None,
43
+ use_tool_retriever: bool | None = None,
44
+ timeout_seconds: int | None = None,
45
+ ):
46
+ # Use default_config values for unspecified parameters
47
+ if path is None:
48
+ path = default_config.path
49
+ if llm is None:
50
+ llm = default_config.llm
51
+ if use_tool_retriever is None:
52
+ use_tool_retriever = default_config.use_tool_retriever
53
+ if timeout_seconds is None:
54
+ timeout_seconds = default_config.timeout_seconds
55
+
56
+ self.path = path
57
+ if not os.path.exists(path):
58
+ os.makedirs(path)
59
+ print(f"Created directory: {path}")
60
+ ### TODO: Download the data
61
+ else:
62
+ print(f"Data directory already exists: {path}, loading...")
63
+
64
+ module2api = read_module2api()
65
+
66
+ self.llm = get_llm(llm, config=default_config)
67
+ tools = []
68
+ for module, api_list in module2api.items():
69
+ print("Registering tools from module:", module)
70
+ tools += [api_schema_to_langchain_tool(api, mode="custom_tool", module_name=module) for api in api_list]
71
+ self.tools = tools
72
+ self.module2api = module2api
73
+ self.use_tool_retriever = use_tool_retriever
74
+
75
+ # Store dictionaries for data lake and library content
76
+ self.data_lake_dict = data_lake_dict
77
+ self.library_content_dict = library_content_dict
78
+
79
+ if self.use_tool_retriever:
80
+ self.tool_registry = ToolRegistry(module2api)
81
+ self.retriever = ToolRetriever()
82
+
83
+ self.timeout_seconds = timeout_seconds # 10 minutes default timeout
84
+
85
+ # When wrapping tools with timeout
86
+ self.tools = self._add_timeout_to_tools(self.tools)
87
+ self.prompt = ""
88
+ self.system_prompt = ""
89
+
90
+ def _add_timeout_to_tools(self, tools):
91
+ """Apply timeout wrapper to all tool functions using multiprocessing."""
92
+
93
+ def create_timed_func(original_func, timeout):
94
+ """Factory function that creates a unique timed function for each tool."""
95
+ tool_name = getattr(original_func, "__name__", "unknown")
96
+ # print(f"Applying timeout wrapper to tool: {tool_name}")
97
+
98
+ def process_func(func, args, kwargs, result_queue):
99
+ """Function to run in a separate process."""
100
+ try:
101
+ result = func(*args, **kwargs)
102
+ result_queue.put(("success", result))
103
+ except Exception as e:
104
+ result_queue.put(("error", str(e)))
105
+
106
+ @wraps(original_func)
107
+ def timed_func(*args, **kwargs):
108
+ # print(f"Executing tool with timeout: {tool_name}")
109
+ result_queue = Queue()
110
+
111
+ # Start a separate process
112
+ proc = Process(
113
+ target=process_func,
114
+ args=(original_func, args, kwargs, result_queue),
115
+ )
116
+ proc.start()
117
+
118
+ # Wait for the specified timeout
119
+ proc.join(timeout)
120
+
121
+ # Check if the process is still running after timeout
122
+ if proc.is_alive():
123
+ print(f"TIMEOUT: Tool {tool_name} execution timed out after {timeout} seconds")
124
+ # Force terminate the process
125
+ proc.terminate()
126
+ proc.join(1) # Give it a second to terminate
127
+
128
+ # If it's still not dead, kill it with more force
129
+ if proc.is_alive():
130
+ os.kill(proc.pid, signal.SIGKILL)
131
+
132
+ return f"ERROR: Tool execution timed out after {timeout} seconds. Please try with simpler inputs or break your task into smaller steps."
133
+
134
+ # Get the result from the queue
135
+ if not result_queue.empty():
136
+ status, result = result_queue.get()
137
+ if status == "success":
138
+ return result
139
+ else:
140
+ return f"Error in tool execution: {result}"
141
+
142
+ return "Error: Tool execution completed but no result was returned"
143
+
144
+ return timed_func
145
+
146
+ wrapped_tools = []
147
+ for tool in tools:
148
+ wrapped_tool = tool
149
+ wrapped_tool.func = create_timed_func(tool.func, self.timeout_seconds)
150
+ wrapped_tools.append(wrapped_tool)
151
+
152
+ return wrapped_tools
153
+
154
+ def add_tool(self, api):
155
+ function_code = inspect.getsource(api)
156
+ schema = function_to_api_schema(function_code, self.llm)
157
+ new_tool = api_schema_to_langchain_tool(schema, mode="custom_tool", module_name=api.__module__)
158
+
159
+ # Create a single wrapped tool using the existing _add_timeout_to_tools method
160
+ wrapped_tools = self._add_timeout_to_tools([new_tool])
161
+
162
+ # Get the wrapped tool and add it to our tools list
163
+ if wrapped_tools:
164
+ self.tools.append(wrapped_tools[0])
165
+
166
+ def configure(
167
+ self,
168
+ plan=False,
169
+ reflect=False,
170
+ data_lake=False,
171
+ react_code_search=False,
172
+ library_access=False,
173
+ ):
174
+ data_lake_path = self.path + "/data_lake"
175
+ data_lake_content = glob.glob(data_lake_path + "/*")
176
+ data_lake_items = [x.split("/")[-1] for x in data_lake_content]
177
+
178
+ if react_code_search:
179
+ tools = [i for i in self.tools if i.name in ["run_python_repl", "search_google"]]
180
+
181
+ prompt_modifier = """
182
+ You are a helpful biomedical assistant assigned with the task of problem-solving.
183
+
184
+ You have access to two tools:
185
+ 1) run_python_repl: to write and run python code
186
+ 2) search_google: to search google for information
187
+
188
+ You can use them to solve the problem.
189
+ """
190
+ else:
191
+ tools = self.tools
192
+ if (not plan) and (not reflect):
193
+ prompt_modifier = """You are a helpful biologist and expert geneticist.
194
+ """
195
+ elif plan and (not reflect):
196
+ prompt_modifier = """You are a helpful biologist and expert geneticist.
197
+ Given the question from the user,
198
+ - First, come up with a high level plan based on your understanding of the problem and available tools and record it in the Research Plan and Status. You can revise the plan later.
199
+ - Research Plan and Status should well organized and succinctly keep track of 1) high level plan (can be revised), 2) what steps have been done and what steps are in progress, 3) short results and conclusions of each step after it has been performed. Do not perform action in research plan.
200
+ - Research Plan and Status must only include progress that has been made by previous steps. It should not include results not directly confirmed by the previous observation.
201
+ - Follow the plan and try to achieve the goal as straightforwardly as possible. Use tools as necessary.
202
+ """
203
+ elif (not plan) and reflect:
204
+ prompt_modifier = """You are a helpful biologist and expert geneticist.
205
+ In each round after the tool is used, conduct "reflection" step: reflect on the current state of the problem and the results of the last round. What does the observation mean? If there is an error, what caused the error and how to debug?
206
+ """
207
+ else:
208
+ prompt_modifier = """You are a helpful biologist and expert geneticist.
209
+ Given the question from the user,
210
+ - First, come up with a high level plan based on your understanding of the problem and available tools and record it in the Research Plan and Status. You can revise the plan later.
211
+ - Research Plan and Status should well organized and succinctly keep track of 1) high level plan (can be revised), 2) what steps have been done and what steps are in progress, 3) short results and conclusions of each step after it has been performed. Do not perform action in research plan.
212
+ - Research Plan and Status must only include progress that has been made by previous steps. It should not include results not directly confirmed by the previous observation.
213
+ - Follow the plan and try to achieve the goal as straightforwardly as possible. Use tools as necessary.
214
+ In each round after the tool is used, conduct "reflection" step: reflect on the current state of the problem and the results of the last round. What does the observation mean? If there is an error, what caused the error and how to debug?
215
+ You have access to write_python_code and run_python_repl tool to write and run your own code if tools fail, or if the given tools are not enough. Please always make sure to write code when dealing with substantial data, including finding the length of long sequences or elements at different positions.
216
+ """
217
+
218
+ if data_lake:
219
+ # Format data lake items with descriptions
220
+ data_lake_formatted = []
221
+ for item in data_lake_items:
222
+ description = data_lake_dict.get(item, f"Data lake item: {item}")
223
+ data_lake_formatted.append(f"{item}: {description}")
224
+
225
+ prompt_modifier += """
226
+ You can also access a biological data lake at the following path: {data_lake_path}. You can use the run_python_repl tool to write code to understand the data, process and utilize it for the task.
227
+ Here is the list of datasets with their descriptions:
228
+ ----
229
+ {data_lake_formatted}
230
+ ----
231
+ """.format(
232
+ data_lake_path=data_lake_path,
233
+ data_lake_formatted="\n".join(data_lake_formatted),
234
+ )
235
+
236
+ if library_access:
237
+ # Format library content with descriptions
238
+ library_formatted = []
239
+ for lib_name, lib_desc in library_content_dict.items():
240
+ library_formatted.append(f"{lib_name}: {lib_desc}")
241
+
242
+ prompt_modifier += """
243
+ You also have access to a list of software packages that can be used to perform various tasks.
244
+ You can use the run_python_repl tool to write code to access and utilize the library for the task.
245
+ Don't forget the import statement.
246
+ Here is the list of available libraries with their descriptions:
247
+ ----
248
+ {library_formatted}
249
+ ----
250
+ """.format(library_formatted="\n".join(library_formatted))
251
+
252
+ print("=" * 25 + "System Prompt" + "=" * 25)
253
+ print(prompt_modifier)
254
+ self.system_prompt = prompt_modifier
255
+ self.prompt = ChatPromptTemplate.from_messages(
256
+ [
257
+ ("system", prompt_modifier),
258
+ MessagesPlaceholder(variable_name="messages"),
259
+ ]
260
+ )
261
+
262
+ # Store the tools for later use
263
+ self.active_tools = tools
264
+
265
+ # Create a custom implementation of the ReAct agent using LangGraph
266
+ self.app = self._create_custom_react_agent(self.llm, tools, self.prompt)
267
+
268
+ def _create_custom_react_agent(self, llm, tools, prompt):
269
+ """Create a custom ReAct agent using LangGraph."""
270
+ # Create a dictionary mapping tool names to tool objects for easy lookup
271
+ tools_by_name = {tool.name: tool for tool in tools}
272
+
273
+ # Bind the tools to the language model
274
+ llm_with_tools = llm.bind_tools(tools)
275
+
276
+ # Define the node that calls the model
277
+ def call_model(state: AgentState, config: RunnableConfig = None):
278
+ """Node that calls the language model to get the next action."""
279
+ system_message = SystemMessage(content=self.system_prompt)
280
+ messages = [system_message] + state["messages"]
281
+ response = llm_with_tools.invoke(messages, config=config)
282
+ return {"messages": [response]}
283
+
284
+ # Define the node that executes tools
285
+ def tool_node(state: AgentState):
286
+ """Node that executes tools based on the LLM's decisions."""
287
+ outputs = []
288
+ for tool_call in state["messages"][-1].tool_calls:
289
+ try:
290
+ tool_result = tools_by_name[tool_call["name"]].invoke(tool_call["args"])
291
+ outputs.append(
292
+ ToolMessage(
293
+ content=json.dumps(tool_result),
294
+ name=tool_call["name"],
295
+ tool_call_id=tool_call["id"],
296
+ )
297
+ )
298
+ except Exception as e:
299
+ # Handle any errors that occur during tool execution
300
+ outputs.append(
301
+ ToolMessage(
302
+ content=json.dumps({"error": str(e)}),
303
+ name=tool_call["name"],
304
+ tool_call_id=tool_call["id"],
305
+ )
306
+ )
307
+ return {"messages": outputs}
308
+
309
+ # Define the conditional edge that determines whether to continue or not
310
+ def should_continue(state: AgentState):
311
+ """Determine if we should continue running the graph or finish."""
312
+ messages = state["messages"]
313
+ last_message = messages[-1]
314
+ # If there is no tool call, then we finish
315
+ if not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
316
+ return "end"
317
+ # Otherwise if there is, we continue
318
+ else:
319
+ return "continue"
320
+
321
+ # Define a new graph
322
+ workflow = StateGraph(AgentState)
323
+
324
+ # Define the two nodes we will cycle between
325
+ workflow.add_node("agent", call_model)
326
+ workflow.add_node("tools", tool_node)
327
+
328
+ # Set the entrypoint as `agent`
329
+ workflow.set_entry_point("agent")
330
+
331
+ # Add conditional edges
332
+ workflow.add_conditional_edges(
333
+ "agent",
334
+ should_continue,
335
+ {
336
+ "continue": "tools",
337
+ "end": END,
338
+ },
339
+ )
340
+
341
+ # Add edge from tools back to agent
342
+ workflow.add_edge("tools", "agent")
343
+
344
+ # Compile the graph
345
+ return workflow.compile()
346
+
347
+ def go(self, prompt):
348
+ """Execute the agent with the given prompt.
349
+
350
+ Args:
351
+ prompt: The user's query
352
+
353
+ """
354
+ if self.use_tool_retriever:
355
+ # Gather all available tools from the registry
356
+ all_tools = self.tool_registry.tools if hasattr(self, "tool_registry") else []
357
+
358
+ # Get data lake items with descriptions
359
+ data_lake_path = self.path + "/data_lake"
360
+ data_lake_content = glob.glob(data_lake_path + "/*")
361
+ data_lake_items = [x.split("/")[-1] for x in data_lake_content]
362
+
363
+ # Create data lake descriptions for retrieval
364
+ data_lake_descriptions = []
365
+ for item in data_lake_items:
366
+ description = self.data_lake_dict.get(item, f"Data lake item: {item}")
367
+ data_lake_descriptions.append({"name": item, "description": description})
368
+
369
+ # Libraries with descriptions
370
+ library_descriptions = []
371
+ for lib_name, lib_desc in self.library_content_dict.items():
372
+ library_descriptions.append({"name": lib_name, "description": lib_desc})
373
+
374
+ # Prepare resources for retrieval
375
+ resources = {
376
+ "tools": all_tools,
377
+ "data_lake": data_lake_descriptions,
378
+ "libraries": library_descriptions,
379
+ }
380
+
381
+ # Use prompt-based retrieval with the agent's LLM
382
+ selected_resources = self.retriever.prompt_based_retrieval(prompt, resources, llm=self.llm)
383
+ print("Using prompt-based retrieval with the agent's LLM")
384
+
385
+ # If we're using prompt or embedding based retrieval, print the selected resources
386
+ print("\nSelected tools:")
387
+ for tool in selected_resources["tools"]:
388
+ if isinstance(tool, dict):
389
+ print(f"- {tool.get('name', 'Unknown')}: {tool.get('description', '')}")
390
+ else:
391
+ print(f"- {getattr(tool, 'name', str(tool))}: {getattr(tool, 'description', '')}")
392
+
393
+ print("\nSelected data lake items:")
394
+ for item in selected_resources["data_lake"]:
395
+ if isinstance(item, dict):
396
+ name = item.get("name", "Unknown")
397
+ description = self.data_lake_dict.get(name, f"Data lake item: {name}")
398
+ print(f"- {name}: {description}")
399
+ elif isinstance(item, str) and ": " in item:
400
+ # If the item already has a description, print it as is
401
+ print(f"- {item}")
402
+ else:
403
+ description = self.data_lake_dict.get(item, f"Data lake item: {item}")
404
+ print(f"- {item}: {description}")
405
+
406
+ print("\nSelected libraries:")
407
+ for lib in selected_resources["libraries"]:
408
+ if isinstance(lib, dict):
409
+ print(f"- {lib.get('name', 'Unknown')}: {lib.get('description', '')}")
410
+ else:
411
+ print(f"- {lib}")
412
+
413
+ # Convert selected tools to langchain tool objects
414
+ tool_names = [
415
+ tool["name"] if isinstance(tool, dict) else getattr(tool, "name", str(tool))
416
+ for tool in selected_resources["tools"]
417
+ ]
418
+ retrieved_list_of_tools = []
419
+
420
+ # Get the tool objects by name
421
+ for tool_name in tool_names:
422
+ # Find the tool in the original tools list
423
+ matching_tools = [t for t in self.tools if getattr(t, "name", None) == tool_name]
424
+ if matching_tools:
425
+ retrieved_list_of_tools.append(matching_tools[0])
426
+
427
+ # Add back coding tool if not already included
428
+ if len([i for i in retrieved_list_of_tools if i.name == "run_python_repl"]) == 0:
429
+ retrieved_list_of_tools = retrieved_list_of_tools + [
430
+ i for i in self.tools if i.name == "run_python_repl"
431
+ ]
432
+
433
+ print("Retrieved tools: \n" + "\n".join([l.name + ": " + l.description for l in retrieved_list_of_tools]))
434
+ # Recreate the custom agent with the retrieved tools
435
+ self.app = self._create_custom_react_agent(self.llm, retrieved_list_of_tools, self.prompt)
436
+
437
+ # Default behavior (no tool retriever or retrieval_method is 'none')
438
+ config = {"recursion_limit": 50}
439
+ inputs = {"messages": [("user", prompt)]}
440
+ self.log = []
441
+ for s in self.app.stream(inputs, stream_mode="values", config=config):
442
+ message = s["messages"][-1]
443
+ out = pretty_print(message)
444
+ self.log.append(out)
445
+ return self.log, s["messages"][-1].content
446
+
447
+ def result_formatting(self, output_class, task_intention):
448
+ self.format_check_prompt = ChatPromptTemplate.from_messages(
449
+ [
450
+ (
451
+ "system",
452
+ (
453
+ "You are evaluateGPT, tasked with extract and parse the task output based on the history of an agent. "
454
+ "Review the entire history of messages provided. "
455
+ "Here is the task output requirement: \n"
456
+ f"'{task_intention.replace('{', '{{').replace('}', '}}')}'.\n"
457
+ ),
458
+ ),
459
+ ("placeholder", "{messages}"),
460
+ ]
461
+ )
462
+
463
+ checker_llm = self.format_check_prompt | self.llm.with_structured_output(output_class)
464
+ result = checker_llm.invoke({"messages": [("user", str(self.log))]}).dict()
465
+ return result
Biomanus_upload/biomni/biorxiv_scripts/extract_biorxiv_tasks.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Script to extract common tasks, databases, and software from bioRxiv papers
3
+ using the PaperTaskExtractor and existing metadata CSV.
4
+ """
5
+
6
+ import argparse
7
+ import io
8
+ import json
9
+ import os
10
+ import random
11
+ import sys
12
+ import time
13
+ from typing import Any
14
+
15
+ import pandas as pd
16
+ import PyPDF2
17
+ import requests
18
+ from tqdm import tqdm
19
+
20
+ # Add the parent directory to the path so we can import the bioagentos package
21
+ sys.path.append("../../")
22
+ from biomni.agent.env_collection import PaperTaskExtractor
23
+
24
+
25
+ def parse_arguments():
26
+ """Parse command line arguments."""
27
+ parser = argparse.ArgumentParser(description="Extract tasks from bioRxiv papers in a specific subject area.")
28
+ parser.add_argument(
29
+ "--subject",
30
+ type=str,
31
+ required=True,
32
+ help='Subject area to search for papers (e.g., "neuroscience", "bioinformatics")',
33
+ )
34
+ parser.add_argument(
35
+ "--limit",
36
+ type=int,
37
+ default=10,
38
+ help="Maximum number of papers to process (default: 10)",
39
+ )
40
+ parser.add_argument(
41
+ "--metadata-path",
42
+ type=str,
43
+ default="<REMOTE_USER_HOME>/BioAgentOS/data/biorxiv_metadata.csv",
44
+ help="Path to bioRxiv metadata CSV file (default: data/biorxiv_metadata.csv)",
45
+ )
46
+ parser.add_argument(
47
+ "--output-dir",
48
+ type=str,
49
+ default=None,
50
+ help="Directory to save results (default: ./biorxiv_results_{subject}_{limit})",
51
+ )
52
+ parser.add_argument(
53
+ "--model",
54
+ type=str,
55
+ default="claude-3-haiku-20240307",
56
+ help="LLM model to use for extraction (default: claude-3-haiku-20240307)",
57
+ )
58
+ parser.add_argument(
59
+ "--chunk-size",
60
+ type=int,
61
+ default=4000,
62
+ help="Chunk size for text processing (default: 4000)",
63
+ )
64
+ parser.add_argument(
65
+ "--chunk-overlap",
66
+ type=int,
67
+ default=400,
68
+ help="Chunk overlap for text processing (default: 400)",
69
+ )
70
+ parser.add_argument(
71
+ "--max-paper-length",
72
+ type=int,
73
+ default=200000,
74
+ help="Maximum paper length in characters (default: 200000)",
75
+ )
76
+ parser.add_argument("--save-pdfs", action="store_true", help="Save downloaded PDFs (default: False)")
77
+ parser.add_argument(
78
+ "--random-sample",
79
+ action="store_true",
80
+ help="Randomly sample papers instead of taking the first N (default: False)",
81
+ )
82
+ return parser.parse_args()
83
+
84
+
85
+ def load_papers_from_csv(metadata_path: str, subject: str, limit: int, random_sample: bool) -> pd.DataFrame:
86
+ """Load papers from bioRxiv metadata CSV file.
87
+
88
+ Args:
89
+ metadata_path: Path to the metadata CSV file
90
+ subject: Subject area to filter by
91
+ limit: Maximum number of papers to return
92
+ random_sample: Whether to randomly sample papers
93
+
94
+ Returns:
95
+ DataFrame of filtered papers
96
+
97
+ """
98
+ try:
99
+ # Load the metadata CSV
100
+ df_biorxiv = pd.read_csv(metadata_path)
101
+
102
+ # Filter published papers in the specified subject
103
+ if subject.lower() == "all":
104
+ filtered_df = df_biorxiv[df_biorxiv.published != "NA"]
105
+ else:
106
+ filtered_df = df_biorxiv[
107
+ (df_biorxiv.published != "NA") & (df_biorxiv.category.str.lower() == subject.lower())
108
+ ]
109
+
110
+ # Sample papers
111
+ if len(filtered_df) > limit:
112
+ filtered_df = filtered_df.sample(limit, random_state=42) if random_sample else filtered_df.head(limit)
113
+
114
+ print(f"Loaded {len(filtered_df)} papers in subject: {subject}")
115
+ return filtered_df
116
+
117
+ except Exception as e:
118
+ print(f"Error loading papers from CSV: {e}")
119
+ return pd.DataFrame()
120
+
121
+
122
+ def download_pdf(paper_doi: str, save_path: str | None = None) -> str | None:
123
+ """Download PDF for a given paper DOI.
124
+
125
+ Args:
126
+ paper_doi: DOI of the paper
127
+ save_path: Path to save the PDF (if None, won't save to disk)
128
+
129
+ Returns:
130
+ PDF text content or None if download failed
131
+
132
+ """
133
+ # Construct PDF URL from DOI
134
+ pdf_url = f"https://www.biorxiv.org/content/{paper_doi}v1.full.pdf"
135
+
136
+ try:
137
+ # Add random delay to avoid rate limiting
138
+ time.sleep(random.uniform(1, 3))
139
+
140
+ response = requests.get(pdf_url, stream=True)
141
+ response.raise_for_status()
142
+
143
+ # Save PDF if requested
144
+ if save_path:
145
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
146
+ with open(save_path, "wb") as f:
147
+ for chunk in response.iter_content(chunk_size=8192):
148
+ f.write(chunk)
149
+
150
+ # Extract text from PDF
151
+ pdf_text = extract_text_from_pdf(io.BytesIO(response.content))
152
+ return pdf_text
153
+
154
+ except requests.exceptions.RequestException as e:
155
+ print(f"Error downloading PDF for {paper_doi}: {e}")
156
+ return None
157
+
158
+
159
+ def extract_text_from_pdf(pdf_file) -> str:
160
+ """Extract text content from a PDF file.
161
+
162
+ Args:
163
+ pdf_file: File-like object containing PDF data
164
+
165
+ Returns:
166
+ Extracted text content
167
+
168
+ """
169
+ try:
170
+ pdf_reader = PyPDF2.PdfReader(pdf_file)
171
+ text = ""
172
+ for page_num in range(len(pdf_reader.pages)):
173
+ page = pdf_reader.pages[page_num]
174
+ text += page.extract_text() + "\n\n"
175
+ return text
176
+ except Exception as e:
177
+ print(f"Error extracting text from PDF: {e}")
178
+ return ""
179
+
180
+
181
+ def truncate_text(text: str, max_length: int) -> str:
182
+ """Truncate text to a maximum length while preserving complete sentences.
183
+
184
+ Args:
185
+ text: The text to truncate
186
+ max_length: Maximum length in characters
187
+
188
+ Returns:
189
+ Truncated text
190
+
191
+ """
192
+ if len(text) <= max_length:
193
+ return text
194
+
195
+ # Find the last sentence boundary before max_length
196
+ truncated = text[:max_length]
197
+ last_period = truncated.rfind(".")
198
+ last_question = truncated.rfind("?")
199
+ last_exclamation = truncated.rfind("!")
200
+
201
+ # Find the last sentence boundary
202
+ last_boundary = max(last_period, last_question, last_exclamation)
203
+
204
+ if last_boundary > 0:
205
+ return text[: last_boundary + 1]
206
+ else:
207
+ # If no sentence boundary found, just truncate at max_length
208
+ return truncated
209
+
210
+
211
+ def process_papers(papers_df: pd.DataFrame, args) -> list[dict[str, Any]]:
212
+ """Process papers using PaperTaskExtractor.
213
+
214
+ Args:
215
+ papers_df: DataFrame of paper metadata
216
+ args: Command line arguments
217
+
218
+ Returns:
219
+ List of papers with extracted tasks, databases, and software
220
+
221
+ """
222
+ # Initialize the task extractor
223
+ extractor = PaperTaskExtractor(llm=args.model, chunk_size=args.chunk_size, chunk_overlap=args.chunk_overlap)
224
+
225
+ results = []
226
+
227
+ for _, paper in tqdm(papers_df.iterrows(), total=len(papers_df), desc="Processing papers"):
228
+ paper_doi = paper.get("doi")
229
+ if not paper_doi:
230
+ continue
231
+
232
+ print(f"\nProcessing paper: {paper.get('title')} (DOI: {paper_doi})")
233
+
234
+ # Create paths
235
+ pdf_path = None
236
+ if args.save_pdfs:
237
+ pdf_path = os.path.join(args.output_dir, "pdfs", f"{paper_doi.replace('/', '_')}.pdf")
238
+
239
+ result_path = os.path.join(args.output_dir, "results", f"{paper_doi.replace('/', '_')}.json")
240
+
241
+ # Skip if already processed
242
+ if os.path.exists(result_path):
243
+ print(f"Paper already processed, loading from {result_path}")
244
+ with open(result_path) as f:
245
+ paper_result = json.load(f)
246
+ results.append(paper_result)
247
+ continue
248
+
249
+ # Download and process PDF
250
+ pdf_text = download_pdf(paper_doi, pdf_path)
251
+ if not pdf_text:
252
+ print(f"Failed to download or extract text from {paper_doi}")
253
+ continue
254
+
255
+ # Truncate text if it exceeds max length
256
+ if len(pdf_text) > args.max_paper_length:
257
+ original_length = len(pdf_text)
258
+ pdf_text = truncate_text(pdf_text, args.max_paper_length)
259
+ print(f"Truncated paper from {original_length} to {len(pdf_text)} characters")
260
+
261
+ # Extract tasks, databases, and software
262
+ try:
263
+ _, extraction_results = extractor.go(pdf_text)
264
+
265
+ # Add paper metadata to results
266
+ paper_result = {
267
+ "metadata": {
268
+ "doi": paper.get("doi"),
269
+ "title": paper.get("title"),
270
+ "authors": paper.get("authors", ""),
271
+ "abstract": paper.get("abstract", ""),
272
+ "date": paper.get("date", ""),
273
+ "category": paper.get("category", ""),
274
+ "text_length": len(pdf_text),
275
+ },
276
+ "extraction": extraction_results,
277
+ }
278
+
279
+ # Save results
280
+ os.makedirs(os.path.dirname(result_path), exist_ok=True)
281
+ with open(result_path, "w") as f:
282
+ json.dump(paper_result, f, indent=2)
283
+
284
+ results.append(paper_result)
285
+
286
+ except Exception as e:
287
+ print(f"Error processing paper {paper_doi}: {e}")
288
+
289
+ return results
290
+
291
+
292
+ def generate_summary(results: list[dict[str, Any]], output_dir: str):
293
+ """Generate summary of extracted tasks, databases, and software.
294
+
295
+ Args:
296
+ results: List of papers with extraction results
297
+ output_dir: Directory to save summary
298
+
299
+ """
300
+ # Collect all tasks, databases, and software
301
+ all_tasks = []
302
+ all_databases = []
303
+ all_software = []
304
+
305
+ for paper in results:
306
+ extraction = paper.get("extraction", {})
307
+
308
+ # Add paper metadata to each item
309
+ paper_meta = paper.get("metadata", {})
310
+ paper_id = f"{paper_meta.get('title')} ({paper_meta.get('doi')})"
311
+
312
+ # Process tasks
313
+ for task in extraction.get("tasks", []):
314
+ task["paper"] = paper_id
315
+ all_tasks.append(task)
316
+
317
+ # Process databases
318
+ for db in extraction.get("databases", []):
319
+ db["paper"] = paper_id
320
+ all_databases.append(db)
321
+
322
+ # Process software
323
+ for sw in extraction.get("software", []):
324
+ sw["paper"] = paper_id
325
+ all_software.append(sw)
326
+
327
+ # Create summary dataframes
328
+ if all_tasks:
329
+ tasks_df = pd.DataFrame(all_tasks)
330
+ tasks_df.to_csv(os.path.join(output_dir, "tasks_summary.csv"), index=False)
331
+
332
+ if all_databases:
333
+ db_df = pd.DataFrame(all_databases)
334
+ db_df.to_csv(os.path.join(output_dir, "databases_summary.csv"), index=False)
335
+
336
+ if all_software:
337
+ sw_df = pd.DataFrame(all_software)
338
+ sw_df.to_csv(os.path.join(output_dir, "software_summary.csv"), index=False)
339
+
340
+ # Generate frequency counts
341
+ task_counts = {}
342
+ for task in all_tasks:
343
+ task_name = task.get("task_name")
344
+ if task_name:
345
+ task_counts[task_name] = task_counts.get(task_name, 0) + 1
346
+
347
+ db_counts = {}
348
+ for db in all_databases:
349
+ db_name = db.get("name")
350
+ if db_name:
351
+ db_counts[db_name] = db_counts.get(db_name, 0) + 1
352
+
353
+ sw_counts = {}
354
+ for sw in all_software:
355
+ sw_name = sw.get("name")
356
+ if sw_name:
357
+ sw_counts[sw_name] = sw_counts.get(sw_name, 0) + 1
358
+
359
+ # Save frequency counts
360
+ with open(os.path.join(output_dir, "frequency_summary.json"), "w") as f:
361
+ json.dump(
362
+ {"tasks": task_counts, "databases": db_counts, "software": sw_counts},
363
+ f,
364
+ indent=2,
365
+ )
366
+
367
+ print(f"Summary files saved to {output_dir}")
368
+
369
+
370
+ def main():
371
+ """Main function to run the script."""
372
+ args = parse_arguments()
373
+
374
+ # Set default output directory if not specified
375
+ if args.output_dir is None:
376
+ # Clean subject name for directory name (replace spaces with underscores, lowercase)
377
+ clean_subject = args.subject.lower().replace(" ", "_").replace("/", "_")
378
+ args.output_dir = f"./biorxiv_results_{clean_subject}_{args.limit}"
379
+
380
+ # Create output directories
381
+ os.makedirs(args.output_dir, exist_ok=True)
382
+ os.makedirs(os.path.join(args.output_dir, "results"), exist_ok=True)
383
+ if args.save_pdfs:
384
+ os.makedirs(os.path.join(args.output_dir, "pdfs"), exist_ok=True)
385
+
386
+ # Load papers from CSV
387
+ print(f"Loading papers from {args.metadata_path} in subject area: {args.subject}")
388
+ papers_df = load_papers_from_csv(args.metadata_path, args.subject, args.limit, args.random_sample)
389
+
390
+ if len(papers_df) == 0:
391
+ print(f"No papers found for subject: {args.subject}")
392
+ return
393
+
394
+ # Process papers
395
+ results = process_papers(papers_df, args)
396
+
397
+ # Generate summary
398
+ generate_summary(results, args.output_dir)
399
+
400
+ print("Done!")
401
+
402
+
403
+ if __name__ == "__main__":
404
+ main()
Biomanus_upload/biomni/biorxiv_scripts/generate_function.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Command-line tool to generate Python functions from task descriptions using the function_generator agent."""
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+
8
+ from biomni.agent.function_generator import FunctionGenerator
9
+ from tqdm import tqdm
10
+
11
+
12
+ def main():
13
+ """Main function for the command-line tool."""
14
+ parser = argparse.ArgumentParser(description="Generate Python functions given task descriptions")
15
+ parser.add_argument(
16
+ "--task",
17
+ "-t",
18
+ type=str,
19
+ help="JSON file containing a list of task descriptions",
20
+ )
21
+ parser.add_argument(
22
+ "--output-dir",
23
+ "-o",
24
+ type=str,
25
+ default="generated_functions",
26
+ help="Directory to save the generated functions (default: generated_functions)",
27
+ )
28
+ parser.add_argument(
29
+ "--model",
30
+ "-m",
31
+ type=str,
32
+ default="claude-3-7-sonnet-latest",
33
+ help="LLM model to use (default: claude-3-7-sonnet-latest)",
34
+ )
35
+ parser.add_argument(
36
+ "--temperature",
37
+ type=float,
38
+ default=0.7,
39
+ help="Temperature setting for the LLM (default: 0.7)",
40
+ )
41
+
42
+ args = parser.parse_args()
43
+
44
+ with open(args.task) as f_tasks:
45
+ task_list = json.load(f_tasks)
46
+ task_descriptions = list(task_list["tasks"])
47
+
48
+ # Create the output directory if it doesn't exist
49
+ os.makedirs(args.output_dir, exist_ok=True)
50
+
51
+ # Initialize the function generator agent
52
+ function_generator = FunctionGenerator(llm=args.model, temperature=args.temperature)
53
+
54
+ if not task_list:
55
+ print("No tasks found.")
56
+ else:
57
+ # tqdm shows a progress bar, file names as description
58
+ for _i, desc in enumerate(tqdm(task_descriptions, desc="Generating Python scripts given task descriptions"), 1):
59
+ generated_script_name, generated_codes = function_generator.go(desc)
60
+
61
+ # Save results
62
+ result_path = os.path.join(args.output_dir, generated_script_name)
63
+
64
+ os.makedirs(os.path.dirname(result_path), exist_ok=True)
65
+ with open(result_path, "w") as f:
66
+ f.write(generated_codes)
67
+
68
+ print("DONE")
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
Biomanus_upload/biomni/biorxiv_scripts/process_all_subjects.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Script to process all bioRxiv subjects and extract computational tasks from the top 100 papers in each."""
3
+
4
+ import argparse
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import time
9
+
10
+ import pandas as pd
11
+ from tqdm import tqdm
12
+
13
+ # Add the parent directory to the path so we can import the bioagentos package
14
+ sys.path.append("<REMOTE_USER_HOME>/BioAgentOS/")
15
+
16
+ # List of all bioRxiv subjects
17
+ BIORXIV_SUBJECTS = [
18
+ "evolutionary biology",
19
+ "ecology",
20
+ "neuroscience",
21
+ "developmental biology",
22
+ "plant biology",
23
+ "microbiology",
24
+ "cancer biology",
25
+ "immunology",
26
+ "cell biology",
27
+ "biochemistry",
28
+ "genetics",
29
+ "bioinformatics",
30
+ "animal behavior and cognition",
31
+ "biophysics",
32
+ "genomics",
33
+ "systems biology",
34
+ "bioengineering",
35
+ "molecular biology",
36
+ "physiology",
37
+ "zoology",
38
+ "scientific communication and education",
39
+ "pathology",
40
+ "synthetic biology",
41
+ "paleontology",
42
+ "pharmacology and toxicology",
43
+ ]
44
+
45
+
46
+ def parse_arguments():
47
+ """Parse command line arguments."""
48
+ parser = argparse.ArgumentParser(description="Process all bioRxiv subjects and extract tasks from papers.")
49
+ parser.add_argument(
50
+ "--papers-per-subject",
51
+ type=int,
52
+ default=100,
53
+ help="Number of papers to process per subject (default: 100)",
54
+ )
55
+ parser.add_argument(
56
+ "--metadata-path",
57
+ type=str,
58
+ default="<REMOTE_USER_HOME>/BioAgentOS/data/biorxiv_metadata.csv",
59
+ help="Path to bioRxiv metadata CSV file (default: data/biorxiv_metadata.csv)",
60
+ )
61
+ parser.add_argument(
62
+ "--output-dir",
63
+ type=str,
64
+ default="./biorxiv_all_subjects",
65
+ help="Base directory to save results (default: ./biorxiv_all_subjects)",
66
+ )
67
+ parser.add_argument(
68
+ "--model",
69
+ type=str,
70
+ default="claude-3-haiku-20240307",
71
+ help="LLM model to use for extraction (default: claude-3-haiku-20240307)",
72
+ )
73
+ parser.add_argument(
74
+ "--max-paper-length",
75
+ type=int,
76
+ default=200000,
77
+ help="Maximum paper length in characters (default: 200000)",
78
+ )
79
+ parser.add_argument(
80
+ "--chunk-size",
81
+ type=int,
82
+ default=10000,
83
+ help="Chunk size for text processing (default: 10000)",
84
+ )
85
+ parser.add_argument(
86
+ "--chunk-overlap",
87
+ type=int,
88
+ default=400,
89
+ help="Chunk overlap for text processing (default: 400)",
90
+ )
91
+ parser.add_argument("--save-pdfs", action="store_true", help="Save downloaded PDFs (default: False)")
92
+ parser.add_argument(
93
+ "--random-sample",
94
+ action="store_true",
95
+ help="Randomly sample papers instead of taking the first N (default: False)",
96
+ )
97
+ parser.add_argument(
98
+ "--subjects",
99
+ type=str,
100
+ nargs="+",
101
+ help="Specific subjects to process (default: all subjects)",
102
+ )
103
+ parser.add_argument(
104
+ "--delay",
105
+ type=int,
106
+ default=60,
107
+ help="Delay in seconds between processing subjects (default: 60)",
108
+ )
109
+ parser.add_argument(
110
+ "--summary-only",
111
+ action="store_true",
112
+ help="Only generate the combined summary (default: False)",
113
+ )
114
+ return parser.parse_args()
115
+
116
+
117
+ def check_subject_availability(metadata_path, subjects):
118
+ """Check which subjects have papers available in the metadata.
119
+
120
+ Args:
121
+ metadata_path: Path to the metadata CSV file
122
+ subjects: List of subjects to check
123
+
124
+ Returns:
125
+ List of subjects with available papers
126
+
127
+ """
128
+ try:
129
+ df_biorxiv = pd.read_csv(metadata_path)
130
+ available_subjects = []
131
+
132
+ for subject in subjects:
133
+ count = len(
134
+ df_biorxiv[(df_biorxiv.published != "NA") & (df_biorxiv.category.str.lower() == subject.lower())]
135
+ )
136
+ if count > 0:
137
+ available_subjects.append((subject, count))
138
+ print(f"Subject '{subject}' has {count} papers available")
139
+ else:
140
+ print(f"Subject '{subject}' has no papers available")
141
+
142
+ return available_subjects
143
+ except Exception as e:
144
+ print(f"Error checking subject availability: {e}")
145
+ return []
146
+
147
+
148
+ def process_subject(subject, args):
149
+ """Process a single subject by calling the extract_biorxiv_tasks.py script.
150
+
151
+ Args:
152
+ subject: Subject to process
153
+ args: Command line arguments
154
+
155
+ """
156
+ # Clean subject name for command line
157
+ subject.replace(" ", "\\ ")
158
+
159
+ # Build command
160
+ cmd = [
161
+ "python",
162
+ "BioAgentOS/bioagentos/scripts/extract_biorxiv_tasks.py",
163
+ "--subject",
164
+ f'"{subject}"',
165
+ "--limit",
166
+ str(args.papers_per_subject),
167
+ "--metadata-path",
168
+ args.metadata_path,
169
+ "--model",
170
+ args.model,
171
+ "--max-paper-length",
172
+ str(args.max_paper_length),
173
+ "--chunk-size",
174
+ str(args.chunk_size),
175
+ "--chunk-overlap",
176
+ str(args.chunk_overlap),
177
+ ]
178
+
179
+ # Add optional arguments
180
+ if args.save_pdfs:
181
+ cmd.append("--save-pdfs")
182
+ if args.random_sample:
183
+ cmd.append("--random-sample")
184
+
185
+ # Convert command list to string
186
+ cmd_str = " ".join(cmd)
187
+
188
+ print(f"\n{'=' * 80}\nProcessing subject: {subject}\n{'=' * 80}")
189
+ print(f"Running command: {cmd_str}")
190
+
191
+ # Execute command
192
+ try:
193
+ subprocess.run(cmd_str, shell=True, check=True)
194
+ print(f"Successfully processed subject: {subject}")
195
+ except subprocess.CalledProcessError as e:
196
+ print(f"Error processing subject '{subject}': {e}")
197
+
198
+
199
+ def combine_summaries(base_dir, subjects, papers_per_subject):
200
+ """Combine frequency summaries from all subjects into a single summary.
201
+
202
+ Args:
203
+ base_dir: Base directory containing subject results
204
+ subjects: List of subjects that were processed
205
+ papers_per_subject: Number of papers processed per subject
206
+
207
+ """
208
+ all_tasks = {}
209
+ all_databases = {}
210
+ all_software = {}
211
+
212
+ for subject, _ in subjects:
213
+ # Clean subject name for directory name
214
+ clean_subject = subject.lower().replace(" ", "_").replace("/", "_")
215
+ subject_dir = f"biorxiv_results_{clean_subject}_{papers_per_subject}"
216
+ summary_path = os.path.join(subject_dir, "frequency_summary.json")
217
+
218
+ try:
219
+ if os.path.exists(summary_path):
220
+ with open(summary_path) as f:
221
+ summary = json.load(f)
222
+
223
+ # Merge tasks
224
+ for task, count in summary.get("tasks", {}).items():
225
+ if task in all_tasks:
226
+ all_tasks[task] += count
227
+ else:
228
+ all_tasks[task] = count
229
+
230
+ # Merge databases
231
+ for db, count in summary.get("databases", {}).items():
232
+ if db in all_databases:
233
+ all_databases[db] += count
234
+ else:
235
+ all_databases[db] = count
236
+
237
+ # Merge software
238
+ for sw, count in summary.get("software", {}).items():
239
+ if sw in all_software:
240
+ all_software[sw] += count
241
+ else:
242
+ all_software[sw] = count
243
+ else:
244
+ print(f"Warning: Summary file not found for subject '{subject}'")
245
+ except Exception as e:
246
+ print(f"Error processing summary for subject '{subject}': {e}")
247
+
248
+ # Sort by frequency (descending)
249
+ all_tasks = dict(sorted(all_tasks.items(), key=lambda item: item[1], reverse=True))
250
+ all_databases = dict(sorted(all_databases.items(), key=lambda item: item[1], reverse=True))
251
+ all_software = dict(sorted(all_software.items(), key=lambda item: item[1], reverse=True))
252
+
253
+ # Create combined summary
254
+ combined_summary = {
255
+ "tasks": all_tasks,
256
+ "databases": all_databases,
257
+ "software": all_software,
258
+ }
259
+
260
+ # Save combined summary
261
+ combined_path = os.path.join(base_dir, "combined_summary.json")
262
+ os.makedirs(base_dir, exist_ok=True)
263
+
264
+ with open(combined_path, "w") as f:
265
+ json.dump(combined_summary, f, indent=2)
266
+
267
+ print(f"Combined summary saved to {combined_path}")
268
+
269
+ # Create CSV files for each category
270
+ tasks_df = pd.DataFrame(list(all_tasks.items()), columns=["Task", "Frequency"])
271
+ tasks_df.to_csv(os.path.join(base_dir, "tasks_frequency.csv"), index=False)
272
+
273
+ db_df = pd.DataFrame(list(all_databases.items()), columns=["Database", "Frequency"])
274
+ db_df.to_csv(os.path.join(base_dir, "databases_frequency.csv"), index=False)
275
+
276
+ sw_df = pd.DataFrame(list(all_software.items()), columns=["Software", "Frequency"])
277
+ sw_df.to_csv(os.path.join(base_dir, "software_frequency.csv"), index=False)
278
+
279
+ print(f"CSV summaries saved to {base_dir}")
280
+
281
+
282
+ def main():
283
+ """Main function to run the script."""
284
+ args = parse_arguments()
285
+
286
+ # Determine which subjects to process
287
+ subjects_to_process = args.subjects if args.subjects else BIORXIV_SUBJECTS
288
+
289
+ # Check which subjects have papers available
290
+ available_subjects = check_subject_availability(args.metadata_path, subjects_to_process)
291
+
292
+ if not available_subjects:
293
+ print("No subjects with available papers found. Exiting.")
294
+ return
295
+
296
+ if not args.summary_only:
297
+ # Process each subject
298
+ for subject, _count in tqdm(available_subjects, desc="Processing subjects"):
299
+ process_subject(subject, args)
300
+
301
+ # Add delay between subjects to avoid rate limiting
302
+ if args.delay > 0:
303
+ print(f"Waiting {args.delay} seconds before processing the next subject...")
304
+ time.sleep(args.delay)
305
+
306
+ # Combine summaries from all subjects
307
+ combine_summaries(args.output_dir, available_subjects, args.papers_per_subject)
308
+
309
+ print("All subjects processed successfully!")
310
+
311
+
312
+ if __name__ == "__main__":
313
+ import json # Import here to avoid circular import with the main script
314
+
315
+ main()
Biomanus_upload/biomni/config.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Biomni Configuration Management
3
+
4
+ Simple configuration class for centralizing common settings.
5
+ Maintains full backward compatibility with existing code.
6
+ """
7
+
8
+ import os
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class BiomniConfig:
14
+ """Central configuration for Biomni agent.
15
+
16
+ All settings are optional and have sensible defaults.
17
+ API keys are still read from environment variables to maintain
18
+ compatibility with existing .env file structure.
19
+
20
+ Usage:
21
+ # Create config with defaults
22
+ config = BiomniConfig()
23
+
24
+ # Override specific settings
25
+ config = BiomniConfig(llm="gpt-4", timeout_seconds=1200)
26
+
27
+ # Modify after creation
28
+ config.path = "./custom_data"
29
+ """
30
+
31
+ # Data and execution settings
32
+ path: str = "./data"
33
+ timeout_seconds: int = 600
34
+
35
+ # LLM settings (API keys still from environment)
36
+ llm: str = "claude-sonnet-4-6"
37
+ temperature: float = 0.7
38
+
39
+ # Tool settings
40
+ use_tool_retriever: bool = True
41
+ rewrite_user_query: bool = True
42
+ dynamic_mcp_registration: bool = True
43
+ mcp_server_top_k: int = 8
44
+ mcp_tool_top_k: int = 12
45
+ mcp_server_candidate_pool: int = 24
46
+
47
+ # Data licensing settings
48
+ commercial_mode: bool = False # If True, excludes non-commercial datasets
49
+
50
+ # Custom model settings (for custom LLM serving)
51
+ base_url: str | None = None
52
+ api_key: str | None = None # Only for custom models, not provider API keys
53
+
54
+ # LLM source (auto-detected if None)
55
+ source: str | None = None
56
+
57
+ # Third-party integrations
58
+ protocols_io_access_token: str | None = None
59
+
60
+ def __post_init__(self):
61
+ """Load any environment variable overrides if they exist."""
62
+ # Check for environment variable overrides (optional)
63
+ # Support both old and new names for backwards compatibility
64
+ if os.getenv("BIOMNI_PATH") or os.getenv("BIOMNI_DATA_PATH"):
65
+ self.path = os.getenv("BIOMNI_PATH") or os.getenv("BIOMNI_DATA_PATH")
66
+ if os.getenv("BIOMNI_TIMEOUT_SECONDS"):
67
+ self.timeout_seconds = int(os.getenv("BIOMNI_TIMEOUT_SECONDS"))
68
+ if os.getenv("BIOMNI_LLM") or os.getenv("BIOMNI_LLM_MODEL"):
69
+ self.llm = os.getenv("BIOMNI_LLM") or os.getenv("BIOMNI_LLM_MODEL")
70
+ if os.getenv("BIOMNI_USE_TOOL_RETRIEVER"):
71
+ self.use_tool_retriever = os.getenv("BIOMNI_USE_TOOL_RETRIEVER").lower() == "true"
72
+ if os.getenv("BIOMNI_REWRITE_USER_QUERY"):
73
+ self.rewrite_user_query = os.getenv("BIOMNI_REWRITE_USER_QUERY").lower() == "true"
74
+ if os.getenv("BIOMNI_DYNAMIC_MCP_REGISTRATION"):
75
+ self.dynamic_mcp_registration = os.getenv("BIOMNI_DYNAMIC_MCP_REGISTRATION").lower() == "true"
76
+ if os.getenv("BIOMNI_MCP_SERVER_TOP_K"):
77
+ self.mcp_server_top_k = int(os.getenv("BIOMNI_MCP_SERVER_TOP_K"))
78
+ if os.getenv("BIOMNI_MCP_TOOL_TOP_K"):
79
+ self.mcp_tool_top_k = int(os.getenv("BIOMNI_MCP_TOOL_TOP_K"))
80
+ if os.getenv("BIOMNI_MCP_SERVER_CANDIDATE_POOL"):
81
+ self.mcp_server_candidate_pool = int(os.getenv("BIOMNI_MCP_SERVER_CANDIDATE_POOL"))
82
+ if os.getenv("BIOMNI_COMMERCIAL_MODE"):
83
+ self.commercial_mode = os.getenv("BIOMNI_COMMERCIAL_MODE").lower() == "true"
84
+ if os.getenv("BIOMNI_TEMPERATURE"):
85
+ self.temperature = float(os.getenv("BIOMNI_TEMPERATURE"))
86
+ if os.getenv("BIOMNI_CUSTOM_BASE_URL"):
87
+ self.base_url = os.getenv("BIOMNI_CUSTOM_BASE_URL")
88
+ if os.getenv("BIOMNI_CUSTOM_API_KEY"):
89
+ self.api_key = os.getenv("BIOMNI_CUSTOM_API_KEY")
90
+ if os.getenv("BIOMNI_SOURCE"):
91
+ self.source = os.getenv("BIOMNI_SOURCE")
92
+
93
+ # Protocols.io access token (prefer specific env vars)
94
+ env_token = os.getenv("PROTOCOLS_IO_ACCESS_TOKEN") or os.getenv("BIOMNI_PROTOCOLS_IO_ACCESS_TOKEN")
95
+ if env_token:
96
+ self.protocols_io_access_token = env_token
97
+
98
+ def to_dict(self) -> dict:
99
+ """Convert config to dictionary for easy access."""
100
+ return {
101
+ "path": self.path,
102
+ "timeout_seconds": self.timeout_seconds,
103
+ "llm": self.llm,
104
+ "temperature": self.temperature,
105
+ "use_tool_retriever": self.use_tool_retriever,
106
+ "rewrite_user_query": self.rewrite_user_query,
107
+ "dynamic_mcp_registration": self.dynamic_mcp_registration,
108
+ "mcp_server_top_k": self.mcp_server_top_k,
109
+ "mcp_tool_top_k": self.mcp_tool_top_k,
110
+ "mcp_server_candidate_pool": self.mcp_server_candidate_pool,
111
+ "commercial_mode": self.commercial_mode,
112
+ "base_url": self.base_url,
113
+ "api_key": self.api_key,
114
+ "source": self.source,
115
+ }
116
+
117
+
118
+ # Global default config instance (optional, for convenience)
119
+ default_config = BiomniConfig()
Biomanus_upload/biomni/env_desc.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data lake dictionary with detailed descriptions
2
+ data_lake_dict = {
3
+ "affinity_capture-ms.parquet": "Protein-protein interactions detected via affinity capture and mass spectrometry.",
4
+ "affinity_capture-rna.parquet": "Protein-RNA interactions detected by affinity capture.",
5
+ "BindingDB_All_202409.tsv": "Measured binding affinities between proteins and small molecules for drug discovery.",
6
+ "broad_repurposing_hub_molecule_with_smiles.parquet": "Molecules from Broad Institute's Drug Repurposing Hub with SMILES annotations.",
7
+ "broad_repurposing_hub_phase_moa_target_info.parquet": "Drug phases, mechanisms of action, and target information from Broad Institute.",
8
+ "co-fractionation.parquet": "Protein-protein interactions from co-fractionation experiments.",
9
+ "czi_census_datasets_v4.parquet": "Datasets from the Chan Zuckerberg Initiative's Cell Census.",
10
+ "DepMap_CRISPRGeneDependency.csv": "Gene dependency probability estimates for cancer cell lines, including all DepMap models.",
11
+ "DepMap_CRISPRGeneEffect.csv": "Genome-wide CRISPR gene effect estimates for cancer cell lines, including all DepMap models.",
12
+ "DepMap_Model.csv": "Metadata describing all cancer models/cell lines which are referenced by a dataset contained within the DepMap portal.",
13
+ "DepMap_OmicsExpressionProteinCodingGenesTPMLogp1.csv": "Gene expression in TPMs for cancer cell lines, including all DepMap models.",
14
+ "ddinter_alimentary_tract_metabolism.csv": "Drug-drug interactions for alimentary tract and metabolism drugs from DDInter 2.0 database.",
15
+ "ddinter_antineoplastic.csv": "Drug-drug interactions for antineoplastic and immunomodulating agents from DDInter 2.0 database.",
16
+ "ddinter_antiparasitic.csv": "Drug-drug interactions for antiparasitic products from DDInter 2.0 database.",
17
+ "ddinter_blood_organs.csv": "Drug-drug interactions for blood and blood forming organs drugs from DDInter 2.0 database.",
18
+ "ddinter_dermatological.csv": "Drug-drug interactions for dermatological drugs from DDInter 2.0 database.",
19
+ "ddinter_hormonal.csv": "Drug-drug interactions for systemic hormonal preparations from DDInter 2.0 database.",
20
+ "ddinter_respiratory.csv": "Drug-drug interactions for respiratory system drugs from DDInter 2.0 database.",
21
+ "ddinter_various.csv": "Drug-drug interactions for various drugs from DDInter 2.0 database.",
22
+ "DisGeNET.parquet": "Gene-disease associations from multiple sources.",
23
+ "dosage_growth_defect.parquet": "Gene dosage changes affecting growth.",
24
+ "enamine_cloud_library_smiles.pkl": "Compounds from Enamine REAL library with SMILES annotations.",
25
+ "evebio_assay_table.csv": "Assay metadata with one row per assay from EveBio pharmome mapping.",
26
+ "evebio_bundle_table.csv": "Target subfamily bundles used for screening-to-profiling progression.",
27
+ "evebio_compound_table.csv": "Compound metadata with common identifiers from EveBio screening.",
28
+ "evebio_control_table.csv": "Control datapoints for all screening and profiling plates.",
29
+ "evebio_detailed_result_table.csv": "Expanded results on evebio_summary_result_table with curve fit parameters and phase categories.",
30
+ "evebio_observed_points_table.csv": "Raw observed datapoints from all screening and profiling experiments.",
31
+ "evebio_summary_result_table.csv": "Succinct summary of results for each assay-compound combination.",
32
+ "evebio_target_table.csv": "Target metadata with common identifiers from EveBio screening.",
33
+ "genebass_missense_LC_filtered.pkl": "Filtered missense variants from GeneBass.",
34
+ "genebass_pLoF_filtered.pkl": "Predicted loss-of-function variants from GeneBass.",
35
+ "genebass_synonymous_filtered.pkl": "Filtered synonymous variants from GeneBass.",
36
+ "gene_info.parquet": "Comprehensive gene information.",
37
+ "genetic_interaction.parquet": "Genetic interactions between genes.",
38
+ "go-plus.json": "Gene ontology data for functional gene annotations.",
39
+ "gtex_tissue_gene_tpm.parquet": "Gene expression (TPM) across human tissues from GTEx.",
40
+ "gwas_catalog.pkl": "Genome-wide association studies (GWAS) results.",
41
+ "hp.obo": "Official HPO release in obographs format",
42
+ "kg.csv": "Precision medicine knowledge graph with 17,080 diseases and 4+ million relationships across biological scales.",
43
+ "marker_celltype.parquet": "Cell type marker genes for identification.",
44
+ "McPAS-TCR.parquet": "T-cell receptor sequences and specificity data from McPAS database.",
45
+ "miRDB_v6.0_results.parquet": "Predicted microRNA targets from miRDB.",
46
+ "miRTarBase_microRNA_target_interaction.parquet": "Experimentally validated microRNA-target interactions from miRTarBase.",
47
+ "miRTarBase_microRNA_target_interaction_pubmed_abtract.txt": "PubMed abstracts for microRNA-target interactions in miRTarBase.",
48
+ "miRTarBase_MicroRNA_Target_Sites.parquet": "Binding sites of microRNAs on target genes from miRTarBase.",
49
+ "mousemine_m1_positional_geneset.parquet": "Positional gene sets from MouseMine.",
50
+ "mousemine_m2_curated_geneset.parquet": "Curated gene sets from MouseMine.",
51
+ "mousemine_m3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MouseMine.",
52
+ "mousemine_m5_ontology_geneset.parquet": "Ontology-based gene sets from MouseMine.",
53
+ "mousemine_m8_celltype_signature_geneset.parquet": "Cell type signature gene sets from MouseMine.",
54
+ "mousemine_mh_hallmark_geneset.parquet": "Hallmark gene sets from MouseMine.",
55
+ "msigdb_human_c1_positional_geneset.parquet": "Human positional gene sets from MSigDB.",
56
+ "msigdb_human_c2_curated_geneset.parquet": "Curated human gene sets from MSigDB.",
57
+ "msigdb_human_c3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MSigDB.",
58
+ "msigdb_human_c3_subset_transcription_factor_targets_from_GTRD.parquet": "Transcription factor targets from GTRD/MSigDB.",
59
+ "msigdb_human_c4_computational_geneset.parquet": "Computationally derived gene sets from MSigDB.",
60
+ "msigdb_human_c5_ontology_geneset.parquet": "Ontology-based gene sets from MSigDB.",
61
+ "msigdb_human_c6_oncogenic_signature_geneset.parquet": "Oncogenic signatures from MSigDB.",
62
+ "msigdb_human_c7_immunologic_signature_geneset.parquet": "Immunologic signatures from MSigDB.",
63
+ "msigdb_human_c8_celltype_signature_geneset.parquet": "Cell type signatures from MSigDB.",
64
+ "msigdb_human_h_hallmark_geneset.parquet": "Hallmark gene sets from MSigDB.",
65
+ "omim.parquet": "Genetic disorders and associated genes from OMIM.",
66
+ "proteinatlas.tsv": "Protein expression data from Human Protein Atlas.",
67
+ "proximity_label-ms.parquet": "Protein interactions via proximity labeling and mass spectrometry.",
68
+ "reconstituted_complex.parquet": "Protein complexes reconstituted in vitro.",
69
+ "sgRNA_KO_SP_mouse.txt": "sgRNA knockout data for mouse.",
70
+ "sgRNA_KO_SP_human.txt": "sgRNA knockout data for human.",
71
+ "synthetic_growth_defect.parquet": "Synthetic growth defects from genetic interactions.",
72
+ "synthetic_lethality.parquet": "Synthetic lethal interactions.",
73
+ "synthetic_rescue.parquet": "Genetic interactions rescuing phenotypes.",
74
+ "two-hybrid.parquet": "Protein-protein interactions detected by yeast two-hybrid assays.",
75
+ "variant_table.parquet": "Annotated genetic variants table.",
76
+ "Virus-Host_PPI_P-HIPSTER_2020.parquet": "Virus-host protein-protein interactions from P-HIPSTER.",
77
+ "txgnn_name_mapping.pkl": "Name mapping for TXGNN.",
78
+ "txgnn_prediction.pkl": "Prediction data for TXGNN.",
79
+ }
80
+
81
+ # Updated library_content as a dictionary with detailed descriptions
82
+ library_content_dict = {
83
+ # === PYTHON PACKAGES ===
84
+ # Core Bioinformatics Libraries (Python)
85
+ "biopython": "[Python Package] A set of tools for biological computation including parsers for bioinformatics files, access to online services, and interfaces to common bioinformatics programs.",
86
+ "biom-format": "[Python Package] The Biological Observation Matrix (BIOM) format is designed for representing biological sample by observation contingency tables with associated metadata.",
87
+ "scanpy": "[Python Package] A scalable toolkit for analyzing single-cell gene expression data, specifically designed for large datasets using AnnData.",
88
+ "scikit-bio": "[Python Package] Data structures, algorithms, and educational resources for bioinformatics, including sequence analysis, phylogenetics, and ordination methods.",
89
+ "anndata": "[Python Package] A Python package for handling annotated data matrices in memory and on disk, primarily used for single-cell genomics data.",
90
+ "mudata": "[Python Package] A Python package for multimodal data storage and manipulation, extending AnnData to handle multiple modalities.",
91
+ "pyliftover": "[Python Package] A Python implementation of UCSC liftOver tool for converting genomic coordinates between genome assemblies.",
92
+ "biopandas": "[Python Package] A package that provides pandas DataFrames for working with molecular structures and biological data.",
93
+ "biotite": "[Python Package] A comprehensive library for computational molecular biology, providing tools for sequence analysis, structure analysis, and more.",
94
+ "lazyslide": "[Python Package] A Python framework that brings interoperable, reproducible whole slide image analysis, enabling seamless histopathology workflows from preprocessing to deep learning.",
95
+ # Genomics & Variant Analysis (Python)
96
+ "gget": "[Python Package] A toolkit for accessing genomic databases and retrieving sequences, annotations, and other genomic data.",
97
+ "lifelines": "[Python Package] A complete survival analysis library for fitting models, plotting, and statistical tests.",
98
+ # "scvi-tools": "[Python Package] A package for probabilistic modeling of single-cell omics data, including deep generative models.",
99
+ "gseapy": "[Python Package] A Python wrapper for Gene Set Enrichment Analysis (GSEA) and visualization.",
100
+ "scrublet": "[Python Package] A tool for detecting doublets in single-cell RNA-seq data.",
101
+ "cellxgene-census": "[Python Package] A tool for accessing and analyzing the CellxGene Census, a collection of single-cell datasets. To download a dataset, use the download_source_h5ad function with the dataset id as the argument (856c1b98-5727-49da-bf0f-151bdb8cb056, no .h5ad extension).",
102
+ "hyperopt": "[Python Package] A Python library for optimizing hyperparameters of machine learning algorithms.",
103
+ "scvelo": "[Python Package] A tool for RNA velocity analysis in single cells using dynamical models.",
104
+ "pysam": "[Python Package] A Python module for reading, manipulating and writing genomic data sets in SAM/BAM/VCF/BCF formats.",
105
+ "pyfaidx": "[Python Package] A Python package for efficient random access to FASTA files.",
106
+ "pyranges": "[Python Package] A Python package for interval manipulation with a pandas-like interface.",
107
+ "pybedtools": "[Python Package] A Python wrapper for Aaron Quinlan's BEDTools programs.",
108
+ # "panhumanpy": "A Python package for hierarchical, cross-tissue cell type annotation of human single-cell RNA-seq data",
109
+ # Structural Biology & Drug Discovery (Python)
110
+ "rdkit": "[Python Package] A collection of cheminformatics and machine learning tools for working with chemical structures and drug discovery.",
111
+ "deeppurpose": "[Python Package] A deep learning library for drug-target interaction prediction and virtual screening.",
112
+ "pyscreener": "[Python Package] A Python package for virtual screening of chemical compounds.",
113
+ "openbabel": "[Python Package] A chemical toolbox designed to speak the many languages of chemical data, supporting file format conversion and molecular modeling.",
114
+ "descriptastorus": "[Python Package] A library for computing molecular descriptors for machine learning applications in drug discovery.",
115
+ # "pymol": "[Python Package] A molecular visualization system for rendering and animating 3D molecular structures.",
116
+ "openmm": "[Python Package] A toolkit for molecular simulation using high-performance GPU computing.",
117
+ "pytdc": "[Python Package] A Python package for Therapeutics Data Commons, providing access to machine learning datasets for drug discovery.",
118
+ # Data Science & Statistical Analysis (Python)
119
+ "pandas": "[Python Package] A fast, powerful, and flexible data analysis and manipulation library for Python.",
120
+ "numpy": "[Python Package] The fundamental package for scientific computing with Python, providing support for arrays, matrices, and mathematical functions.",
121
+ "scipy": "[Python Package] A Python library for scientific and technical computing, including modules for optimization, linear algebra, integration, and statistics.",
122
+ "scikit-learn": "[Python Package] A machine learning library featuring various classification, regression, and clustering algorithms.",
123
+ "matplotlib": "[Python Package] A comprehensive library for creating static, animated, and interactive visualizations in Python.",
124
+ "seaborn": "[Python Package] A statistical data visualization library based on matplotlib with a high-level interface for drawing attractive statistical graphics.",
125
+ "statsmodels": "[Python Package] A Python module for statistical modeling and econometrics, including descriptive statistics and estimation of statistical models.",
126
+ "pymc3": "[Python Package] A Python package for Bayesian statistical modeling and probabilistic machine learning.",
127
+ # "pystan": "[Python Package] A Python interface to Stan, a platform for statistical modeling and high-performance statistical computation.",
128
+ "umap-learn": "[Python Package] Uniform Manifold Approximation and Projection, a dimension reduction technique.",
129
+ "faiss-cpu": "[Python Package] A library for efficient similarity search and clustering of dense vectors.",
130
+ "harmony-pytorch": "[Python Package] A PyTorch implementation of the Harmony algorithm for integrating single-cell data.",
131
+ # General Bioinformatics & Computational Utilities (Python)
132
+ "tiledb": "[Python Package] A powerful engine for storing and analyzing large-scale genomic data.",
133
+ "tiledbsoma": "[Python Package] A library for working with the SOMA (Stack of Matrices) format using TileDB.",
134
+ "h5py": "[Python Package] A Python interface to the HDF5 binary data format, allowing storage of large amounts of numerical data.",
135
+ "tqdm": "[Python Package] A fast, extensible progress bar for loops and CLI applications.",
136
+ "joblib": "[Python Package] A set of tools to provide lightweight pipelining in Python, including transparent disk-caching and parallel computing.",
137
+ "opencv-python": "[Python Package] OpenCV library for computer vision tasks, useful for image analysis in biological contexts.",
138
+ "PyPDF2": "[Python Package] A library for working with PDF files, useful for extracting text from scientific papers.",
139
+ "googlesearch-python": "[Python Package] A library for performing Google searches programmatically.",
140
+ "scikit-image": "[Python Package] A collection of algorithms for image processing in Python.",
141
+ "pymed": "[Python Package] A Python library for accessing PubMed articles.",
142
+ "arxiv": "[Python Package] A Python wrapper for the arXiv API, allowing access to scientific papers.",
143
+ "scholarly": "[Python Package] A module to retrieve author and publication information from Google Scholar.",
144
+ "cryosparc-tools": "[Python Package] Tools for working with cryoSPARC, a platform for cryo-EM data processing.",
145
+ "mageck": "[Python Package] Analysis of CRISPR screen data.",
146
+ "igraph": "[Python Package] Network analysis and visualization.",
147
+ "pyscenic": "[Python Package] Analysis of single-cell RNA-seq data and gene regulatory networks.",
148
+ "cooler": "[Python Package] Storage and analysis of Hi-C data.",
149
+ "trackpy": "[Python Package] Particle tracking in images and video.",
150
+ "nnunet": "[Python Package] A deep learning framework for biomedical image segmentation, providing a standardized approach to training and inference.",
151
+ # "flowcytometrytools": "[Python Package] Analysis and visualization of flow cytometry data.",
152
+ "cellpose": "[Python Package] Cell segmentation in microscopy images.",
153
+ "viennarna": "[Python Package] RNA secondary structure prediction.",
154
+ "PyMassSpec": "[Python Package] Mass spectrometry data analysis.",
155
+ "python-libsbml": "[Python Package] Working with SBML files for computational biology.",
156
+ "cobra": "[Python Package] Constraint-based modeling of metabolic networks.",
157
+ "reportlab": "[Python Package] Creation of PDF documents.",
158
+ "flowkit": "[Python Package] Toolkit for processing flow cytometry data.",
159
+ "hmmlearn": "[Python Package] Hidden Markov model analysis.",
160
+ "msprime": "[Python Package] Simulation of genetic variation.",
161
+ "tskit": "[Python Package] Handling tree sequences and population genetics data.",
162
+ "cyvcf2": "[Python Package] Fast parsing of VCF files.",
163
+ "pykalman": "[Python Package] Kalman filter and smoother implementation.",
164
+ "fanc": "[Python Package] Analysis of chromatin conformation data.",
165
+ "loompy": "A Python implementation of the Loom file format for efficiently storing and working with large omics datasets.",
166
+ "pyBigWig": "A Python library for accessing bigWig and bigBed files for genome browser track data.",
167
+ "pymzml": "A Python module for high-throughput bioinformatics analysis of mass spectrometry data.",
168
+ "optlang": "A Python package for modeling optimization problems symbolically.",
169
+ "FlowIO": "A Python package for reading and writing flow cytometry data files.",
170
+ "FlowUtils": "Utilities for processing and analyzing flow cytometry data.",
171
+ "arboreto": "A Python package for inferring gene regulatory networks from single-cell RNA-seq data.",
172
+ "pdbfixer": "A Python package for fixing problems in PDB files in preparation for molecular simulations.",
173
+ # === R PACKAGES ===
174
+ # Core R Packages for Data Analysis
175
+ "ggplot2": "[R Package] A system for declaratively creating graphics, based on The Grammar of Graphics. Use with subprocess.run(['Rscript', '-e', 'library(ggplot2); ...']).",
176
+ "dplyr": "[R Package] A grammar of data manipulation, providing a consistent set of verbs that help you solve the most common data manipulation challenges. Use with subprocess.",
177
+ "tidyr": "[R Package] A package that helps you create tidy data, where each column is a variable, each row is an observation, and each cell is a single value. Use with subprocess.",
178
+ "readr": "[R Package] A fast and friendly way to read rectangular data like CSV, TSV, and FWF. Use with subprocess.run(['Rscript', '-e', 'library(readr); ...']).",
179
+ "stringr": "[R Package] A cohesive set of functions designed to make working with strings as easy as possible. Use with subprocess calls.",
180
+ "Matrix": "[R Package] A package that provides classes and methods for dense and sparse matrices. Required for Seurat. Use with subprocess calls.",
181
+ # "Rcpp": "[R Package] Seamless R and C++ Integration, allowing R functions to call compiled C++ code. Use with subprocess calls.",
182
+ # "devtools": "[R Package] Tools to make developing R packages easier, including functions to install packages from GitHub. Use with subprocess calls.",
183
+ # "remotes": "[R Package] Install R packages from GitHub, GitLab, Bitbucket, or other remote repositories. Use with subprocess calls.",
184
+ # Bioinformatics R Packages
185
+ "DESeq2": "[R Package] Differential gene expression analysis based on the negative binomial distribution. Use with subprocess.run(['Rscript', '-e', 'library(DESeq2); ...']).",
186
+ "clusterProfiler": "[R Package] A package for statistical analysis and visualization of functional profiles for genes and gene clusters. Use with subprocess calls.",
187
+ # "DADA2": "[R Package] A package for modeling and correcting Illumina-sequenced amplicon errors. Use with subprocess calls.",
188
+ # "xcms": "[R Package] A package for processing and visualization of LC-MS and GC-MS data. Use with subprocess calls.",
189
+ # "FlowCore": "[R Package] Basic infrastructure for flow cytometry data. Use with subprocess calls.",
190
+ "edgeR": "[R Package] Empirical Analysis of Digital Gene Expression Data in R, for differential expression analysis. Use with subprocess calls.",
191
+ "limma": "[R Package] Linear Models for Microarray Data, for differential expression analysis. Use with subprocess calls.",
192
+ "harmony": "[R Package] A method for integrating and analyzing single-cell data across datasets. Use with subprocess calls.",
193
+ "WGCNA": "[R Package] Weighted Correlation Network Analysis for studying biological networks. Use with subprocess calls.",
194
+ # === CLI TOOLS ===
195
+ # Sequence Analysis Tools
196
+ "samtools": "[CLI Tool] A suite of programs for interacting with high-throughput sequencing data. Use with subprocess.run(['samtools', ...]).",
197
+ "bowtie2": "[CLI Tool] An ultrafast and memory-efficient tool for aligning sequencing reads to long reference sequences. Use with subprocess.run(['bowtie2', ...]).",
198
+ "bwa": "[CLI Tool] Burrows-Wheeler Aligner for mapping low-divergent sequences against a large reference genome. Use with subprocess.run(['bwa', ...]).",
199
+ "bedtools": "[CLI Tool] A powerful toolset for genome arithmetic, allowing operations like intersect, merge, count, and complement on genomic features. Use with subprocess.run(['bedtools', ...]).",
200
+ "macs2": "[CLI Tool] Model-based Analysis of ChIP-Seq data, a tool for identifying transcript factor binding sites.",
201
+ # Quality Control and Processing Tools
202
+ "fastqc": "[CLI Tool] A quality control tool for high throughput sequence data. Use with subprocess.run(['fastqc', ...]).",
203
+ "trimmomatic": "[CLI Tool] A flexible read trimming tool for Illumina NGS data. Use with subprocess.run(['trimmomatic', ...]).",
204
+ # Multiple Sequence Alignment and Phylogenetics
205
+ "mafft": "[CLI Tool] A multiple sequence alignment program for unix-like operating systems. Use with subprocess.run(['mafft', ...]).",
206
+ "Homer": "[CLI Tool] Motif discovery and next-gen sequencing analysis.",
207
+ "FastTree": "[CLI Tool] Phylogenetic trees from sequence alignments.",
208
+ "muscle": "[CLI Tool] Multiple sequence alignment tool.",
209
+ # Genetic Analysis Tools
210
+ "plink": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink', ...]).",
211
+ "plink2": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink2', ...]).",
212
+ "gcta64": "[CLI Tool] Genome-wide Complex Trait Analysis (GCTA) tool for estimating the proportion of phenotypic variance explained by genome-wide SNPs and analyzing genetic relationships. Use with subprocess.run(['gcta64', ...]).",
213
+ "iqtree2": "[CLI Tool] An efficient phylogenetic software for maximum likelihood analysis with built-in model selection and ultrafast bootstrap. Use with subprocess.run(['iqtree2', ...]).",
214
+ "ADFR": "AutoDock for Receptors suite for molecular docking and virtual screening. ",
215
+ "diamond": "A sequence aligner for protein and translated DNA searches, designed for high performance analysis of big sequence data. ",
216
+ "fcsparser": "A command-line tool for parsing and analyzing flow cytometry standard (FCS) files. ",
217
+ "plannotate": "[CLI Tool] A tool for annotating plasmid sequences with common features. ",
218
+ "vina": "[CLI Tool] An open-source program for molecular docking and virtual screening, known for its speed and accuracy improvements over AutoDock 4.",
219
+ "autosite": "[CLI Tool] A binding site detection tool used to identify potential ligand binding pockets on protein structures for molecular docking.",
220
+ "PyLabRobot": "[Python Package] A Python package for controlling liquid-handling robots and other lab automation equipment.",
221
+ }
Biomanus_upload/biomni/env_desc_cm.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data lake dictionary with detailed descriptions (Commercial Mode - Non-commercial datasets commented out)
2
+ data_lake_dict = {
3
+ "affinity_capture-ms.parquet": "Protein-protein interactions detected via affinity capture and mass spectrometry.",
4
+ "affinity_capture-rna.parquet": "Protein-RNA interactions detected by affinity capture.",
5
+ # "BindingDB_All_202409.tsv": "Measured binding affinities between proteins and small molecules for drug discovery.", # Requires commercial license
6
+ "broad_repurposing_hub_molecule_with_smiles.parquet": "Molecules from Broad Institute's Drug Repurposing Hub with SMILES annotations.",
7
+ "broad_repurposing_hub_phase_moa_target_info.parquet": "Drug phases, mechanisms of action, and target information from Broad Institute.",
8
+ "co-fractionation.parquet": "Protein-protein interactions from co-fractionation experiments.",
9
+ "czi_census_datasets_v4.parquet": "Datasets from the Chan Zuckerberg Initiative's Cell Census.",
10
+ "DepMap_CRISPRGeneDependency.csv": "Gene dependency probability estimates for cancer cell lines, including all DepMap models.",
11
+ "DepMap_CRISPRGeneEffect.csv": "Genome-wide CRISPR gene effect estimates for cancer cell lines, including all DepMap models.",
12
+ "DepMap_Model.csv": "Metadata describing all cancer models/cell lines which are referenced by a dataset contained within the DepMap portal.",
13
+ "DepMap_OmicsExpressionProteinCodingGenesTPMLogp1.csv": "Gene expression in TPMs for cancer cell lines, including all DepMap models.",
14
+ # "ddinter_alimentary_tract_metabolism.csv": "Drug-drug interactions for alimentary tract and metabolism drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
15
+ # "ddinter_antineoplastic.csv": "Drug-drug interactions for antineoplastic and immunomodulating agents from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
16
+ # "ddinter_antiparasitic.csv": "Drug-drug interactions for antiparasitic products from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
17
+ # "ddinter_blood_organs.csv": "Drug-drug interactions for blood and blood forming organs drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
18
+ # "ddinter_dermatological.csv": "Drug-drug interactions for dermatological drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
19
+ # "ddinter_hormonal.csv": "Drug-drug interactions for systemic hormonal preparations from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
20
+ # "ddinter_respiratory.csv": "Drug-drug interactions for respiratory system drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
21
+ # "ddinter_various.csv": "Drug-drug interactions for various drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
22
+ # "DisGeNET.parquet": "Gene-disease associations from multiple sources.", # CC BY-NC-SA 4.0 - Non-commercial only
23
+ "dosage_growth_defect.parquet": "Gene dosage changes affecting growth.",
24
+ # "enamine_cloud_library_smiles.pkl": "Compounds from Enamine REAL library with SMILES annotations.", # Proprietary - Requires license
25
+ # "evebio_assay_table.csv": "Assay metadata with one row per assay from EveBio pharmome mapping.", # Proprietary - Requires permission
26
+ # "evebio_bundle_table.csv": "Target subfamily bundles used for screening-to-profiling progression.", # Proprietary - Requires permission
27
+ # "evebio_compound_table.csv": "Compound metadata with common identifiers from EveBio screening.", # Proprietary - Requires permission
28
+ # "evebio_control_table.csv": "Control datapoints for all screening and profiling plates.", # Proprietary - Requires permission
29
+ # "evebio_detailed_result_table.csv": "Expanded results on evebio_summary_result_table with curve fit parameters and phase categories.", # Proprietary - Requires permission
30
+ # "evebio_observed_points_table.csv": "Raw observed datapoints from all screening and profiling experiments.", # Proprietary - Requires permission
31
+ # "evebio_summary_result_table.csv": "Succinct summary of results for each assay-compound combination.", # Proprietary - Requires permission
32
+ # "evebio_target_table.csv": "Target metadata with common identifiers from EveBio screening.", # Proprietary - Requires permission
33
+ "genebass_missense_LC_filtered.pkl": "Filtered missense variants from GeneBass.",
34
+ "genebass_pLoF_filtered.pkl": "Predicted loss-of-function variants from GeneBass.",
35
+ "genebass_synonymous_filtered.pkl": "Filtered synonymous variants from GeneBass.",
36
+ "gene_info.parquet": "Comprehensive gene information.",
37
+ "genetic_interaction.parquet": "Genetic interactions between genes.",
38
+ "go-plus.json": "Gene ontology data for functional gene annotations.",
39
+ "gtex_tissue_gene_tpm.parquet": "Gene expression (TPM) across human tissues from GTEx.",
40
+ "gwas_catalog.pkl": "Genome-wide association studies (GWAS) results.",
41
+ "hp.obo": "Official HPO release in obographs format",
42
+ "kg.csv": "Precision medicine knowledge graph with 17,080 diseases and 4+ million relationships across biological scales.",
43
+ "marker_celltype.parquet": "Cell type marker genes for identification.",
44
+ # "McPAS-TCR.parquet": "T-cell receptor sequences and specificity data from McPAS database.", # CC BY-NC-SA 4.0 - Non-commercial only
45
+ # "miRDB_v6.0_results.parquet": "Predicted microRNA targets from miRDB.", # Non-commercial use only
46
+ # "miRTarBase_microRNA_target_interaction.parquet": "Experimentally validated microRNA-target interactions from miRTarBase.", # CC BY-NC 4.0 - Non-commercial only
47
+ # "miRTarBase_microRNA_target_interaction_pubmed_abtract.txt": "PubMed abstracts for microRNA-target interactions in miRTarBase.", # CC BY-NC 4.0 - Non-commercial only
48
+ # "miRTarBase_MicroRNA_Target_Sites.parquet": "Binding sites of microRNAs on target genes from miRTarBase.", # CC BY-NC 4.0 - Non-commercial only
49
+ "mousemine_m1_positional_geneset.parquet": "Positional gene sets from MouseMine.",
50
+ "mousemine_m2_curated_geneset.parquet": "Curated gene sets from MouseMine.",
51
+ "mousemine_m3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MouseMine.",
52
+ "mousemine_m5_ontology_geneset.parquet": "Ontology-based gene sets from MouseMine.",
53
+ "mousemine_m8_celltype_signature_geneset.parquet": "Cell type signature gene sets from MouseMine.",
54
+ "mousemine_mh_hallmark_geneset.parquet": "Hallmark gene sets from MouseMine.",
55
+ # "msigdb_human_c1_positional_geneset.parquet": "Human positional gene sets from MSigDB.", # Requires commercial license
56
+ # "msigdb_human_c2_curated_geneset.parquet": "Curated human gene sets from MSigDB.", # Requires commercial license
57
+ # "msigdb_human_c3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MSigDB.", # Requires commercial license
58
+ # "msigdb_human_c3_subset_transcription_factor_targets_from_GTRD.parquet": "Transcription factor targets from GTRD/MSigDB.", # Requires commercial license
59
+ # "msigdb_human_c4_computational_geneset.parquet": "Computationally derived gene sets from MSigDB.", # Requires commercial license
60
+ # "msigdb_human_c5_ontology_geneset.parquet": "Ontology-based gene sets from MSigDB.", # Requires commercial license
61
+ # "msigdb_human_c6_oncogenic_signature_geneset.parquet": "Oncogenic signatures from MSigDB.", # Requires commercial license
62
+ # "msigdb_human_c7_immunologic_signature_geneset.parquet": "Immunologic signatures from MSigDB.", # Requires commercial license
63
+ # "msigdb_human_c8_celltype_signature_geneset.parquet": "Cell type signatures from MSigDB.", # Requires commercial license
64
+ # "msigdb_human_h_hallmark_geneset.parquet": "Hallmark gene sets from MSigDB.", # Requires commercial license
65
+ # "omim.parquet": "Genetic disorders and associated genes from OMIM.", # Requires commercial license
66
+ "proteinatlas.tsv": "Protein expression data from Human Protein Atlas.",
67
+ "proximity_label-ms.parquet": "Protein interactions via proximity labeling and mass spectrometry.",
68
+ "reconstituted_complex.parquet": "Protein complexes reconstituted in vitro.",
69
+ "sgRNA_KO_SP_mouse.txt": "sgRNA knockout data for mouse.",
70
+ "sgRNA_KO_SP_human.txt": "sgRNA knockout data for human.",
71
+ "synthetic_growth_defect.parquet": "Synthetic growth defects from genetic interactions.",
72
+ "synthetic_lethality.parquet": "Synthetic lethal interactions.",
73
+ "synthetic_rescue.parquet": "Genetic interactions rescuing phenotypes.",
74
+ "two-hybrid.parquet": "Protein-protein interactions detected by yeast two-hybrid assays.",
75
+ "variant_table.parquet": "Annotated genetic variants table.",
76
+ "Virus-Host_PPI_P-HIPSTER_2020.parquet": "Virus-host protein-protein interactions from P-HIPSTER.",
77
+ "txgnn_name_mapping.pkl": "Name mapping for TXGNN.",
78
+ "txgnn_prediction.pkl": "Prediction data for TXGNN.",
79
+ }
80
+
81
+ # Updated library_content as a dictionary with detailed descriptions
82
+ library_content_dict = {
83
+ # === PYTHON PACKAGES ===
84
+ # Core Bioinformatics Libraries (Python)
85
+ "biopython": "[Python Package] A set of tools for biological computation including parsers for bioinformatics files, access to online services, and interfaces to common bioinformatics programs.",
86
+ "biom-format": "[Python Package] The Biological Observation Matrix (BIOM) format is designed for representing biological sample by observation contingency tables with associated metadata.",
87
+ "scanpy": "[Python Package] A scalable toolkit for analyzing single-cell gene expression data, specifically designed for large datasets using AnnData.",
88
+ "scikit-bio": "[Python Package] Data structures, algorithms, and educational resources for bioinformatics, including sequence analysis, phylogenetics, and ordination methods.",
89
+ "anndata": "[Python Package] A Python package for handling annotated data matrices in memory and on disk, primarily used for single-cell genomics data.",
90
+ "mudata": "[Python Package] A Python package for multimodal data storage and manipulation, extending AnnData to handle multiple modalities.",
91
+ "pyliftover": "[Python Package] A Python implementation of UCSC liftOver tool for converting genomic coordinates between genome assemblies.",
92
+ "biopandas": "[Python Package] A package that provides pandas DataFrames for working with molecular structures and biological data.",
93
+ "biotite": "[Python Package] A comprehensive library for computational molecular biology, providing tools for sequence analysis, structure analysis, and more.",
94
+ "lazyslide": "[Python Package] A Python framework that brings interoperable, reproducible whole slide image analysis, enabling seamless histopathology workflows from preprocessing to deep learning.",
95
+ # Genomics & Variant Analysis (Python)
96
+ "gget": "[Python Package] A toolkit for accessing genomic databases and retrieving sequences, annotations, and other genomic data.",
97
+ "lifelines": "[Python Package] A complete survival analysis library for fitting models, plotting, and statistical tests.",
98
+ # "scvi-tools": "[Python Package] A package for probabilistic modeling of single-cell omics data, including deep generative models.",
99
+ "gseapy": "[Python Package] A Python wrapper for Gene Set Enrichment Analysis (GSEA) and visualization.",
100
+ "scrublet": "[Python Package] A tool for detecting doublets in single-cell RNA-seq data.",
101
+ "cellxgene-census": "[Python Package] A tool for accessing and analyzing the CellxGene Census, a collection of single-cell datasets. To download a dataset, use the download_source_h5ad function with the dataset id as the argument (856c1b98-5727-49da-bf0f-151bdb8cb056, no .h5ad extension).",
102
+ "hyperopt": "[Python Package] A Python library for optimizing hyperparameters of machine learning algorithms.",
103
+ "scvelo": "[Python Package] A tool for RNA velocity analysis in single cells using dynamical models.",
104
+ "pysam": "[Python Package] A Python module for reading, manipulating and writing genomic data sets in SAM/BAM/VCF/BCF formats.",
105
+ "pyfaidx": "[Python Package] A Python package for efficient random access to FASTA files.",
106
+ "pyranges": "[Python Package] A Python package for interval manipulation with a pandas-like interface.",
107
+ "pybedtools": "[Python Package] A Python wrapper for Aaron Quinlan's BEDTools programs.",
108
+ # "panhumanpy": "A Python package for hierarchical, cross-tissue cell type annotation of human single-cell RNA-seq data",
109
+ # Structural Biology & Drug Discovery (Python)
110
+ "rdkit": "[Python Package] A collection of cheminformatics and machine learning tools for working with chemical structures and drug discovery.",
111
+ "deeppurpose": "[Python Package] A deep learning library for drug-target interaction prediction and virtual screening.",
112
+ "pyscreener": "[Python Package] A Python package for virtual screening of chemical compounds.",
113
+ "openbabel": "[Python Package] A chemical toolbox designed to speak the many languages of chemical data, supporting file format conversion and molecular modeling.",
114
+ "descriptastorus": "[Python Package] A library for computing molecular descriptors for machine learning applications in drug discovery.",
115
+ # "pymol": "[Python Package] A molecular visualization system for rendering and animating 3D molecular structures.",
116
+ "openmm": "[Python Package] A toolkit for molecular simulation using high-performance GPU computing.",
117
+ "pytdc": "[Python Package] A Python package for Therapeutics Data Commons, providing access to machine learning datasets for drug discovery.",
118
+ # Data Science & Statistical Analysis (Python)
119
+ "pandas": "[Python Package] A fast, powerful, and flexible data analysis and manipulation library for Python.",
120
+ "numpy": "[Python Package] The fundamental package for scientific computing with Python, providing support for arrays, matrices, and mathematical functions.",
121
+ "scipy": "[Python Package] A Python library for scientific and technical computing, including modules for optimization, linear algebra, integration, and statistics.",
122
+ "scikit-learn": "[Python Package] A machine learning library featuring various classification, regression, and clustering algorithms.",
123
+ "matplotlib": "[Python Package] A comprehensive library for creating static, animated, and interactive visualizations in Python.",
124
+ "seaborn": "[Python Package] A statistical data visualization library based on matplotlib with a high-level interface for drawing attractive statistical graphics.",
125
+ "statsmodels": "[Python Package] A Python module for statistical modeling and econometrics, including descriptive statistics and estimation of statistical models.",
126
+ "pymc3": "[Python Package] A Python package for Bayesian statistical modeling and probabilistic machine learning.",
127
+ # "pystan": "[Python Package] A Python interface to Stan, a platform for statistical modeling and high-performance statistical computation.",
128
+ "umap-learn": "[Python Package] Uniform Manifold Approximation and Projection, a dimension reduction technique.",
129
+ "faiss-cpu": "[Python Package] A library for efficient similarity search and clustering of dense vectors.",
130
+ "harmony-pytorch": "[Python Package] A PyTorch implementation of the Harmony algorithm for integrating single-cell data.",
131
+ # General Bioinformatics & Computational Utilities (Python)
132
+ "tiledb": "[Python Package] A powerful engine for storing and analyzing large-scale genomic data.",
133
+ "tiledbsoma": "[Python Package] A library for working with the SOMA (Stack of Matrices) format using TileDB.",
134
+ "h5py": "[Python Package] A Python interface to the HDF5 binary data format, allowing storage of large amounts of numerical data.",
135
+ "tqdm": "[Python Package] A fast, extensible progress bar for loops and CLI applications.",
136
+ "joblib": "[Python Package] A set of tools to provide lightweight pipelining in Python, including transparent disk-caching and parallel computing.",
137
+ "opencv-python": "[Python Package] OpenCV library for computer vision tasks, useful for image analysis in biological contexts.",
138
+ "PyPDF2": "[Python Package] A library for working with PDF files, useful for extracting text from scientific papers.",
139
+ "googlesearch-python": "[Python Package] A library for performing Google searches programmatically.",
140
+ "scikit-image": "[Python Package] A collection of algorithms for image processing in Python.",
141
+ "pymed": "[Python Package] A Python library for accessing PubMed articles.",
142
+ "arxiv": "[Python Package] A Python wrapper for the arXiv API, allowing access to scientific papers.",
143
+ "scholarly": "[Python Package] A module to retrieve author and publication information from Google Scholar.",
144
+ "cryosparc-tools": "[Python Package] Tools for working with cryoSPARC, a platform for cryo-EM data processing.",
145
+ "mageck": "[Python Package] Analysis of CRISPR screen data.",
146
+ "igraph": "[Python Package] Network analysis and visualization.",
147
+ "pyscenic": "[Python Package] Analysis of single-cell RNA-seq data and gene regulatory networks.",
148
+ "cooler": "[Python Package] Storage and analysis of Hi-C data.",
149
+ "trackpy": "[Python Package] Particle tracking in images and video.",
150
+ # "flowcytometrytools": "[Python Package] Analysis and visualization of flow cytometry data.",
151
+ "cellpose": "[Python Package] Cell segmentation in microscopy images.",
152
+ "viennarna": "[Python Package] RNA secondary structure prediction.",
153
+ "PyMassSpec": "[Python Package] Mass spectrometry data analysis.",
154
+ "python-libsbml": "[Python Package] Working with SBML files for computational biology.",
155
+ "cobra": "[Python Package] Constraint-based modeling of metabolic networks.",
156
+ "reportlab": "[Python Package] Creation of PDF documents.",
157
+ "flowkit": "[Python Package] Toolkit for processing flow cytometry data.",
158
+ "hmmlearn": "[Python Package] Hidden Markov model analysis.",
159
+ "msprime": "[Python Package] Simulation of genetic variation.",
160
+ "tskit": "[Python Package] Handling tree sequences and population genetics data.",
161
+ "cyvcf2": "[Python Package] Fast parsing of VCF files.",
162
+ "pykalman": "[Python Package] Kalman filter and smoother implementation.",
163
+ "fanc": "[Python Package] Analysis of chromatin conformation data.",
164
+ "loompy": "A Python implementation of the Loom file format for efficiently storing and working with large omics datasets.",
165
+ "pyBigWig": "A Python library for accessing bigWig and bigBed files for genome browser track data.",
166
+ "pymzml": "A Python module for high-throughput bioinformatics analysis of mass spectrometry data.",
167
+ "optlang": "A Python package for modeling optimization problems symbolically.",
168
+ "FlowIO": "A Python package for reading and writing flow cytometry data files.",
169
+ "FlowUtils": "Utilities for processing and analyzing flow cytometry data.",
170
+ "arboreto": "A Python package for inferring gene regulatory networks from single-cell RNA-seq data.",
171
+ "pdbfixer": "A Python package for fixing problems in PDB files in preparation for molecular simulations.",
172
+ # === R PACKAGES ===
173
+ # Core R Packages for Data Analysis
174
+ "ggplot2": "[R Package] A system for declaratively creating graphics, based on The Grammar of Graphics. Use with subprocess.run(['Rscript', '-e', 'library(ggplot2); ...']).",
175
+ "dplyr": "[R Package] A grammar of data manipulation, providing a consistent set of verbs that help you solve the most common data manipulation challenges. Use with subprocess.",
176
+ "tidyr": "[R Package] A package that helps you create tidy data, where each column is a variable, each row is an observation, and each cell is a single value. Use with subprocess.",
177
+ "readr": "[R Package] A fast and friendly way to read rectangular data like CSV, TSV, and FWF. Use with subprocess.run(['Rscript', '-e', 'library(readr); ...']).",
178
+ "stringr": "[R Package] A cohesive set of functions designed to make working with strings as easy as possible. Use with subprocess calls.",
179
+ "Matrix": "[R Package] A package that provides classes and methods for dense and sparse matrices. Required for Seurat. Use with subprocess calls.",
180
+ # "Rcpp": "[R Package] Seamless R and C++ Integration, allowing R functions to call compiled C++ code. Use with subprocess calls.",
181
+ # "devtools": "[R Package] Tools to make developing R packages easier, including functions to install packages from GitHub. Use with subprocess calls.",
182
+ # "remotes": "[R Package] Install R packages from GitHub, GitLab, Bitbucket, or other remote repositories. Use with subprocess calls.",
183
+ # Bioinformatics R Packages
184
+ "DESeq2": "[R Package] Differential gene expression analysis based on the negative binomial distribution. Use with subprocess.run(['Rscript', '-e', 'library(DESeq2); ...']).",
185
+ "clusterProfiler": "[R Package] A package for statistical analysis and visualization of functional profiles for genes and gene clusters. Use with subprocess calls.",
186
+ # "DADA2": "[R Package] A package for modeling and correcting Illumina-sequenced amplicon errors. Use with subprocess calls.",
187
+ # "xcms": "[R Package] A package for processing and visualization of LC-MS and GC-MS data. Use with subprocess calls.",
188
+ # "FlowCore": "[R Package] Basic infrastructure for flow cytometry data. Use with subprocess calls.",
189
+ "edgeR": "[R Package] Empirical Analysis of Digital Gene Expression Data in R, for differential expression analysis. Use with subprocess calls.",
190
+ "limma": "[R Package] Linear Models for Microarray Data, for differential expression analysis. Use with subprocess calls.",
191
+ "harmony": "[R Package] A method for integrating and analyzing single-cell data across datasets. Use with subprocess calls.",
192
+ "WGCNA": "[R Package] Weighted Correlation Network Analysis for studying biological networks. Use with subprocess calls.",
193
+ # === CLI TOOLS ===
194
+ # Sequence Analysis Tools
195
+ "samtools": "[CLI Tool] A suite of programs for interacting with high-throughput sequencing data. Use with subprocess.run(['samtools', ...]).",
196
+ "bowtie2": "[CLI Tool] An ultrafast and memory-efficient tool for aligning sequencing reads to long reference sequences. Use with subprocess.run(['bowtie2', ...]).",
197
+ "bwa": "[CLI Tool] Burrows-Wheeler Aligner for mapping low-divergent sequences against a large reference genome. Use with subprocess.run(['bwa', ...]).",
198
+ "bedtools": "[CLI Tool] A powerful toolset for genome arithmetic, allowing operations like intersect, merge, count, and complement on genomic features. Use with subprocess.run(['bedtools', ...]).",
199
+ "macs2": "[CLI Tool] Model-based Analysis of ChIP-Seq data, a tool for identifying transcript factor binding sites.",
200
+ # Quality Control and Processing Tools
201
+ "fastqc": "[CLI Tool] A quality control tool for high throughput sequence data. Use with subprocess.run(['fastqc', ...]).",
202
+ "trimmomatic": "[CLI Tool] A flexible read trimming tool for Illumina NGS data. Use with subprocess.run(['trimmomatic', ...]).",
203
+ # Multiple Sequence Alignment and Phylogenetics
204
+ "mafft": "[CLI Tool] A multiple sequence alignment program for unix-like operating systems. Use with subprocess.run(['mafft', ...]).",
205
+ "Homer": "[CLI Tool] Motif discovery and next-gen sequencing analysis.",
206
+ "FastTree": "[CLI Tool] Phylogenetic trees from sequence alignments.",
207
+ "muscle": "[CLI Tool] Multiple sequence alignment tool.",
208
+ # Genetic Analysis Tools
209
+ "plink": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink', ...]).",
210
+ "plink2": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink2', ...]).",
211
+ "gcta64": "[CLI Tool] Genome-wide Complex Trait Analysis (GCTA) tool for estimating the proportion of phenotypic variance explained by genome-wide SNPs and analyzing genetic relationships. Use with subprocess.run(['gcta64', ...]).",
212
+ "iqtree2": "[CLI Tool] An efficient phylogenetic software for maximum likelihood analysis with built-in model selection and ultrafast bootstrap. Use with subprocess.run(['iqtree2', ...]).",
213
+ "ADFR": "AutoDock for Receptors suite for molecular docking and virtual screening. ",
214
+ "diamond": "A sequence aligner for protein and translated DNA searches, designed for high performance analysis of big sequence data. ",
215
+ "fcsparser": "A command-line tool for parsing and analyzing flow cytometry standard (FCS) files. ",
216
+ "plannotate": "[CLI Tool] A tool for annotating plasmid sequences with common features. ",
217
+ "vina": "[CLI Tool] An open-source program for molecular docking and virtual screening, known for its speed and accuracy improvements over AutoDock 4.",
218
+ "autosite": "[CLI Tool] A binding site detection tool used to identify potential ligand binding pockets on protein structures for molecular docking.",
219
+ }
Biomanus_upload/biomni/eval/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .biomni_eval1 import BiomniEval1
2
+
3
+ __all__ = ["BiomniEval1"]
Biomanus_upload/biomni/eval/biomni_eval1.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BiomniEval1: Evaluation loader for Biomni tasks
3
+
4
+ This class provides a unified interface to evaluate user answers against ground truth
5
+ for all tasks in the BiomniEval1 benchmark.
6
+ """
7
+
8
+ import json
9
+ from typing import Any
10
+
11
+ import pandas as pd
12
+
13
+
14
+ class BiomniEval1:
15
+ """
16
+ Evaluation loader for BiomniEval1 benchmark
17
+
18
+ Usage:
19
+ evaluator = BiomniEval1('biomni_eval1_dataset.parquet')
20
+ score = evaluator.evaluate('gwas_causal_gene_opentargets', 0, 'BRCA1')
21
+ """
22
+
23
+ def __init__(self):
24
+ """
25
+ Initialize the BiomniEval1 evaluator
26
+
27
+ Args:
28
+ dataset_path: Path to the merged dataset parquet file
29
+ """
30
+
31
+ self.df = pd.read_parquet("hf://datasets/biomni/Eval1/biomni_eval1_dataset.parquet")
32
+
33
+ # Create index mapping for fast lookup using task_instance_id
34
+ self.instance_map = {}
35
+ for idx, row in self.df.iterrows():
36
+ key = (row["task_name"], row["task_instance_id"])
37
+ self.instance_map[key] = idx
38
+
39
+ print(f"Loaded BiomniEval1 dataset: {len(self.df)} instances across {self.df['task_name'].nunique()} tasks")
40
+
41
+ def evaluate(self, task_name: str, task_instance_id: int, user_answer: str) -> float:
42
+ """
43
+ Evaluate a user's answer for a given task and instance
44
+
45
+ Args:
46
+ task_name: Name of the task (e.g., 'gwas_causal_gene_opentargets')
47
+ task_instance_id: Task-specific instance ID (not the global instance_id)
48
+ user_answer: User's answer (format depends on task)
49
+
50
+ Returns:
51
+ float: Reward score (0.0 to 1.0)
52
+ """
53
+ # Look up the instance in the dataset using task_instance_id
54
+ key = (task_name, task_instance_id)
55
+ if key not in self.instance_map:
56
+ raise ValueError(f"Instance not found: task={task_name}, task_instance_id={task_instance_id}")
57
+
58
+ df_idx = self.instance_map[key]
59
+ row = self.df.iloc[df_idx]
60
+ ground_truth = row["answer"]
61
+
62
+ # Call task-specific evaluation function
63
+ try:
64
+ reward = self._compute_reward(task_name, user_answer, ground_truth)
65
+ return float(reward)
66
+
67
+ except Exception as e:
68
+ # Preserve original traceback context for easier debugging
69
+ raise RuntimeError(f"Error computing reward for {task_name} instance {task_instance_id}: {e}") from e
70
+
71
+ def _compute_reward(self, task_name: str, user_answer: str, ground_truth: str) -> float:
72
+ """Compute reward using task-specific logic"""
73
+
74
+ if task_name == "crispr_delivery":
75
+ # CRISPR expects answer as a letter (a-f), exact match
76
+ return 1.0 if user_answer.strip().lower() == ground_truth.strip().lower() else 0.0
77
+
78
+ elif task_name.startswith("gwas_causal_gene"):
79
+ # GWAS causal gene expects exact gene match (case-insensitive)
80
+ return 1.0 if user_answer.strip().upper() == ground_truth.strip().upper() else 0.0
81
+
82
+ elif task_name == "gwas_variant_prioritization":
83
+ # GWAS variant expects exact variant match
84
+ return 1.0 if user_answer.strip() == ground_truth.strip() else 0.0
85
+
86
+ elif task_name == "hle":
87
+ # HLE expects letter answer (A-Z), case-insensitive
88
+ return 1.0 if user_answer.strip().upper() == ground_truth.strip().upper() else 0.0
89
+
90
+ elif task_name.startswith("lab_bench"):
91
+ # Lab bench expects letter answer (A-Z), case-insensitive
92
+ return 1.0 if user_answer.strip().upper() == ground_truth.strip().upper() else 0.0
93
+
94
+ elif task_name == "rare_disease_diagnosis":
95
+ # Rare disease expects JSON with OMIM_ID match
96
+ # Parse both user answer and ground truth
97
+ try:
98
+ if isinstance(user_answer, str):
99
+ try:
100
+ user_dict = json.loads(user_answer)
101
+ except json.JSONDecodeError:
102
+ import ast
103
+
104
+ user_dict = ast.literal_eval(user_answer)
105
+ else:
106
+ user_dict = user_answer
107
+
108
+ if isinstance(ground_truth, str):
109
+ gt_dict = json.loads(ground_truth)
110
+ else:
111
+ gt_dict = ground_truth
112
+
113
+ # Compare OMIM_ID
114
+ return 1.0 if user_dict.get("OMIM_ID") == gt_dict.get("OMIM_ID") else 0.0
115
+
116
+ except Exception:
117
+ return 0.0
118
+
119
+ elif task_name == "screen_gene_retrieval":
120
+ # Screen gene retrieval expects gene symbol (case-insensitive)
121
+ return 1.0 if user_answer.strip().upper() == ground_truth.strip().upper() else 0.0
122
+
123
+ elif task_name == "patient_gene_detection":
124
+ # Patient gene detection expects JSON with causal_gene list
125
+ # Ground truth is a comma-separated string or single gene ID
126
+ try:
127
+ if isinstance(user_answer, str):
128
+ try:
129
+ user_dict = json.loads(user_answer)
130
+ except json.JSONDecodeError:
131
+ import ast
132
+
133
+ user_dict = ast.literal_eval(user_answer)
134
+ else:
135
+ user_dict = user_answer
136
+
137
+ # Get predicted genes
138
+ predicted_genes = user_dict.get("causal_gene", [])
139
+ if not isinstance(predicted_genes, list):
140
+ predicted_genes = [predicted_genes]
141
+
142
+ # Get ground truth genes (stored as comma-separated or single)
143
+ if "," in ground_truth:
144
+ true_genes = [g.strip() for g in ground_truth.split(",")]
145
+ else:
146
+ true_genes = [ground_truth]
147
+
148
+ # Check for intersection
149
+ if predicted_genes and set(true_genes) & set(predicted_genes):
150
+ return 1.0
151
+ else:
152
+ return 0.0
153
+
154
+ except Exception:
155
+ return 0.0
156
+
157
+ else:
158
+ raise ValueError(f"Unknown task: {task_name}")
159
+
160
+ def get_instance(self, task_name: str, task_instance_id: int) -> dict[str, Any]:
161
+ """
162
+ Get information about a specific instance
163
+
164
+ Args:
165
+ task_name: Name of the task
166
+ task_instance_id: Task-specific instance ID
167
+
168
+ Returns:
169
+ dict: Instance information including prompt, answer, etc.
170
+ """
171
+ key = (task_name, task_instance_id)
172
+ if key not in self.instance_map:
173
+ raise ValueError(f"Instance not found: task={task_name}, task_instance_id={task_instance_id}")
174
+
175
+ df_idx = self.instance_map[key]
176
+ row = self.df.iloc[df_idx]
177
+
178
+ return {
179
+ "global_instance_id": row["instance_id"],
180
+ "task_instance_id": row["task_instance_id"],
181
+ "task_name": row["task_name"],
182
+ "split": row["split"],
183
+ "prompt": row["prompt"],
184
+ "answer": row["answer"],
185
+ }
186
+
187
+ def list_tasks(self) -> list:
188
+ """Get list of all available tasks"""
189
+ return sorted(self.df["task_name"].unique().tolist())
190
+
191
+ def get_task_stats(self, task_name: str = None) -> dict[str, Any]:
192
+ """
193
+ Get statistics for a task or all tasks
194
+
195
+ Args:
196
+ task_name: Optional task name to filter by
197
+
198
+ Returns:
199
+ dict: Statistics including counts by split
200
+ """
201
+ if task_name:
202
+ task_df = self.df[self.df["task_name"] == task_name]
203
+ if len(task_df) == 0:
204
+ raise ValueError(f"Task not found: {task_name}")
205
+ else:
206
+ task_df = self.df
207
+
208
+ stats = {
209
+ "total_instances": len(task_df),
210
+ "train_instances": len(task_df[task_df["split"] == "train"]),
211
+ "val_instances": len(task_df[task_df["split"] == "val"]),
212
+ }
213
+
214
+ if not task_name:
215
+ stats["tasks"] = {}
216
+ for tn in self.list_tasks():
217
+ stats["tasks"][tn] = self.get_task_stats(tn)
218
+
219
+ return stats
220
+
221
+ def batch_evaluate(self, evaluations: list) -> list:
222
+ """
223
+ Evaluate multiple instances at once
224
+
225
+ Args:
226
+ evaluations: List of tuples (task_name, task_instance_id, user_answer)
227
+
228
+ Returns:
229
+ list: List of reward scores
230
+ """
231
+ results = []
232
+ for task_name, task_instance_id, user_answer in evaluations:
233
+ try:
234
+ score = self.evaluate(task_name, task_instance_id, user_answer)
235
+ results.append(score)
236
+ except Exception as e:
237
+ print(f"Error evaluating {task_name} instance {task_instance_id}: {e}")
238
+ results.append(0.0)
239
+
240
+ return results
241
+
242
+ def get_instances_by_task(self, task_name: str, split: str = None) -> pd.DataFrame:
243
+ """
244
+ Get all instances for a specific task
245
+
246
+ Args:
247
+ task_name: Name of the task
248
+ split: Optional split filter ('train' or 'val')
249
+
250
+ Returns:
251
+ DataFrame with instances
252
+ """
253
+ task_df = self.df[self.df["task_name"] == task_name]
254
+
255
+ if split:
256
+ task_df = task_df[task_df["split"] == split]
257
+
258
+ return task_df.copy()
259
+
260
+ def __repr__(self):
261
+ return f"BiomniEval1(instances={len(self.df)}, tasks={self.df['task_name'].nunique()})"
262
+
263
+ def __len__(self):
264
+ return len(self.df)
265
+
266
+
267
+ def main():
268
+ """Demo usage of BiomniEval1"""
269
+ evaluator = BiomniEval1()
270
+
271
+ print("\nAvailable tasks:")
272
+ for task in evaluator.list_tasks():
273
+ print(f" - {task}")
274
+
275
+ print("\nOverall statistics:")
276
+ stats = evaluator.get_task_stats()
277
+ print(f" Total instances: {stats['total_instances']}")
278
+ print(f" Train: {stats['train_instances']}, Val: {stats['val_instances']}")
279
+
280
+ print("\nPer-task statistics:")
281
+ for task_name in evaluator.list_tasks():
282
+ task_stats = evaluator.get_task_stats(task_name)
283
+ print(
284
+ f" {task_name}: {task_stats['total_instances']} total ({task_stats['train_instances']} train, {task_stats['val_instances']} val)"
285
+ )
286
+
287
+ # Example evaluation
288
+ print("\n" + "=" * 60)
289
+ print("Example evaluation:")
290
+ print("=" * 60)
291
+
292
+ # Get first instance from gwas_variant_prioritization
293
+ first_instance = evaluator.df[evaluator.df["task_name"] == "gwas_variant_prioritization"].iloc[0]
294
+ task_name = first_instance["task_name"]
295
+ task_instance_id = first_instance["task_instance_id"]
296
+ ground_truth = first_instance["answer"]
297
+
298
+ print(f"\nTask: {task_name}")
299
+ print(f"Task Instance ID: {task_instance_id}")
300
+ print(f"Ground truth: {ground_truth}")
301
+ print(f"Prompt preview: {first_instance['prompt'][:200]}...")
302
+
303
+ # Test with correct answer
304
+ score = evaluator.evaluate(task_name, task_instance_id, ground_truth)
305
+ print(f"\nScore (correct answer '{ground_truth}'): {score}")
306
+
307
+ # Test with wrong answer
308
+ score = evaluator.evaluate(task_name, task_instance_id, "wrong_answer")
309
+ print(f"Score (wrong answer 'wrong_answer'): {score}")
310
+
311
+ # Batch evaluation example
312
+ print("\n" + "=" * 60)
313
+ print("Batch evaluation example:")
314
+ print("=" * 60)
315
+ batch_evals = [
316
+ (task_name, task_instance_id, ground_truth), # Correct
317
+ (task_name, task_instance_id, "wrong"), # Wrong
318
+ ]
319
+ scores = evaluator.batch_evaluate(batch_evals)
320
+ print(f"Batch scores: {scores}")
321
+
322
+
323
+ if __name__ == "__main__":
324
+ main()
Biomanus_upload/biomni/graph/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from biomni.graph.graph_rag import MCPGraphRAGRetriever
2
+ from biomni.graph.graph_router import GraphRouter
3
+ from biomni.graph.planning_agent import GraphPlanningAgent
4
+ from biomni.graph.schema_extractor import ToolSchemaExtractor
5
+ from biomni.graph.tool_graph import ToolGraph
6
+ from biomni.graph.workflow_priors import WORKFLOW_PRIORS, WorkflowPrior, rank_workflow_priors
7
+
8
+ __all__ = [
9
+ "GraphPlanningAgent",
10
+ "GraphRouter",
11
+ "MCPGraphRAGRetriever",
12
+ "ToolSchemaExtractor",
13
+ "ToolGraph",
14
+ "WORKFLOW_PRIORS",
15
+ "WorkflowPrior",
16
+ "rank_workflow_priors",
17
+ ]
Biomanus_upload/biomni/graph/graph_rag.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from biomni.graph.tool_graph import ToolGraph
6
+
7
+
8
+ class MCPGraphRAGRetriever:
9
+ """Generic GraphRAG retriever over the full MCP tool graph.
10
+
11
+ This class intentionally avoids benchmark- or task-specific allow/deny
12
+ policies. It only retrieves a compact graph context from semantic overlap
13
+ between the query and graph nodes; the agentic planner decides how to use
14
+ the retrieved graph.
15
+ """
16
+
17
+ TOKEN_PATTERN = re.compile(r"[a-z0-9_+-]+")
18
+ STAGE_RANK = {
19
+ "input_acquisition": 0,
20
+ "preprocessing": 1,
21
+ "analysis": 2,
22
+ "downstream": 3,
23
+ "reporting": 4,
24
+ }
25
+ GENERIC_TOKENS = {
26
+ "answer",
27
+ "based",
28
+ "biological",
29
+ "case",
30
+ "choice",
31
+ "choices",
32
+ "data",
33
+ "following",
34
+ "question",
35
+ "result",
36
+ "task",
37
+ "tool",
38
+ "using",
39
+ "which",
40
+ }
41
+
42
+ def retrieve(
43
+ self,
44
+ *,
45
+ query_context: dict,
46
+ query_semantics: dict,
47
+ tool_graph: ToolGraph,
48
+ candidate_pool: int = 24,
49
+ top_k_tools: int = 12,
50
+ ) -> dict:
51
+ query_tokens = self._query_tokens(query_context, query_semantics)
52
+ query_operations = set(query_semantics.get("operations", []))
53
+ query_capabilities = set(query_semantics.get("capabilities", []))
54
+ query_datatypes = set(query_semantics.get("datatypes", []))
55
+ query_stages = set(query_semantics.get("stages", []))
56
+
57
+ candidates = []
58
+ for server_name, payload in tool_graph.server_index.items():
59
+ server_entry = tool_graph.get_server_entry(server_name) or {}
60
+ server_text = self._server_text(server_name, server_entry, payload)
61
+ server_tokens = self._tokenize(server_text)
62
+ server_overlap = query_tokens & server_tokens
63
+
64
+ for tool in payload.get("tools", []):
65
+ score, reasons = self._score_tool(
66
+ server_name=server_name,
67
+ server_tokens=server_tokens,
68
+ server_overlap=server_overlap,
69
+ tool=tool,
70
+ query_tokens=query_tokens,
71
+ query_operations=query_operations,
72
+ query_capabilities=query_capabilities,
73
+ query_datatypes=query_datatypes,
74
+ query_stages=query_stages,
75
+ tool_graph=tool_graph,
76
+ )
77
+ if score <= 0:
78
+ continue
79
+ candidates.append(
80
+ {
81
+ "server": server_name,
82
+ "tool": tool.get("name"),
83
+ "node_id": tool.get("node_id"),
84
+ "stage": tool.get("stage") or "analysis",
85
+ "operations": tool.get("operations", []),
86
+ "capabilities": tool.get("capabilities", []),
87
+ "consumes": tool.get("consumes", []),
88
+ "produces": tool.get("produces", []),
89
+ "score": round(score, 3),
90
+ "reason": "; ".join(reasons[:6]),
91
+ "graph_context": self._expand_tool_context(tool, tool_graph),
92
+ }
93
+ )
94
+
95
+ candidates.sort(key=lambda item: (item["score"], item["server"], item.get("tool") or ""), reverse=True)
96
+ shortlist = candidates[: max(candidate_pool, top_k_tools)]
97
+ compact = self._compact_subgraph(shortlist[:top_k_tools], tool_graph)
98
+ return {
99
+ "retriever": "mcp_graph_rag",
100
+ "task_profile": {
101
+ "categories": [str(item).lower() for item in query_context.get("categories", [])],
102
+ "operations": sorted(query_operations),
103
+ "capabilities": sorted(query_capabilities),
104
+ "datatypes": sorted(query_datatypes),
105
+ "stages": sorted(query_stages),
106
+ },
107
+ "query_tokens": sorted(query_tokens),
108
+ "candidate_tools": shortlist,
109
+ "compact_subgraph": compact,
110
+ "excluded_servers": [],
111
+ "operation_path_hint": self._operation_path_hint(shortlist[:top_k_tools]),
112
+ "context_text": self._format_context(shortlist[:top_k_tools], compact),
113
+ }
114
+
115
+ def _query_tokens(self, query_context: dict, query_semantics: dict) -> set[str]:
116
+ parts = [
117
+ query_context.get("question", ""),
118
+ query_context.get("original_query", ""),
119
+ query_context.get("rewritten_query", ""),
120
+ query_context.get("retrieval_query", ""),
121
+ " ".join(query_context.get("categories", [])),
122
+ " ".join(query_semantics.get("keywords", [])),
123
+ " ".join(query_semantics.get("operations", [])),
124
+ " ".join(query_semantics.get("capabilities", [])),
125
+ " ".join(query_semantics.get("datatypes", [])),
126
+ ]
127
+ tokens = self._tokenize(" ".join(str(part) for part in parts if part))
128
+ return {token for token in tokens if token not in self.GENERIC_TOKENS and len(token) >= 3}
129
+
130
+ def _score_tool(
131
+ self,
132
+ *,
133
+ server_name: str,
134
+ server_tokens: set[str],
135
+ server_overlap: set[str],
136
+ tool: dict,
137
+ query_tokens: set[str],
138
+ query_operations: set[str],
139
+ query_capabilities: set[str],
140
+ query_datatypes: set[str],
141
+ query_stages: set[str],
142
+ tool_graph: ToolGraph,
143
+ ) -> tuple[float, list[str]]:
144
+ text = " ".join(
145
+ [
146
+ server_name,
147
+ str(tool.get("name", "")),
148
+ str(tool.get("description", "")),
149
+ " ".join(tool.get("keywords", [])),
150
+ " ".join(tool.get("operations", [])),
151
+ " ".join(tool.get("capabilities", [])),
152
+ " ".join(tool.get("consumes", [])),
153
+ " ".join(tool.get("produces", [])),
154
+ ]
155
+ ).lower()
156
+ tool_tokens = self._tokenize(text)
157
+ score = 0.0
158
+ reasons = []
159
+
160
+ tool_overlap = query_tokens & tool_tokens
161
+ if tool_overlap:
162
+ score += min(len(tool_overlap), 10) * 0.8
163
+ reasons.append("tool_token_overlap=" + ",".join(sorted(list(tool_overlap))[:6]))
164
+ if server_overlap:
165
+ score += min(len(server_overlap), 6) * 0.35
166
+ reasons.append("server_token_overlap=" + ",".join(sorted(list(server_overlap))[:4]))
167
+
168
+ tool_operations = set(tool.get("operations", []))
169
+ operation_overlap = query_operations & tool_operations
170
+ if operation_overlap:
171
+ score += len(operation_overlap) * 4.0
172
+ score += len(operation_overlap) * (1.5 / max(1, len(tool_operations)))
173
+ reasons.append("operation_overlap=" + ",".join(sorted(operation_overlap)))
174
+ if len(tool_operations) > 3:
175
+ score -= min(len(tool_operations) - 3, 6) * 0.35
176
+ reasons.append("broad_operation_penalty")
177
+
178
+ capability_overlap = query_capabilities & set(tool.get("capabilities", []))
179
+ if capability_overlap:
180
+ score += len(capability_overlap) * 2.5
181
+ reasons.append("capability_overlap=" + ",".join(sorted(capability_overlap)))
182
+
183
+ tool_datatypes = set(tool.get("consumes", [])) | set(tool.get("produces", []))
184
+ datatype_overlap = query_datatypes & tool_datatypes
185
+ if datatype_overlap:
186
+ score += len(datatype_overlap) * 2.0
187
+ reasons.append("datatype_overlap=" + ",".join(sorted(datatype_overlap)))
188
+
189
+ stage = tool.get("stage")
190
+ if stage and stage in query_stages:
191
+ score += 1.0
192
+ reasons.append("stage_overlap=" + stage)
193
+
194
+ graph_bonus = self._graph_relevance_bonus(tool, query_operations, query_capabilities, query_datatypes, tool_graph)
195
+ if graph_bonus:
196
+ score += graph_bonus
197
+ reasons.append("graph_neighbor_overlap")
198
+
199
+ if not reasons and query_tokens & server_tokens:
200
+ score += 0.5
201
+ reasons.append("weak_server_match")
202
+
203
+ return score, reasons
204
+
205
+ def _graph_relevance_bonus(
206
+ self,
207
+ tool: dict,
208
+ query_operations: set[str],
209
+ query_capabilities: set[str],
210
+ query_datatypes: set[str],
211
+ tool_graph: ToolGraph,
212
+ ) -> float:
213
+ node_id = tool.get("node_id")
214
+ if not node_id:
215
+ return 0.0
216
+ bonus = 0.0
217
+ for edge in tool_graph.adjacency.get(node_id, []):
218
+ target = tool_graph.nodes.get(edge.get("target"), {})
219
+ label = str(target.get("label", "")).lower()
220
+ node_type = target.get("type")
221
+ if node_type == "operation" and label in query_operations:
222
+ bonus += 2.0
223
+ elif node_type == "capability" and label in query_capabilities:
224
+ bonus += 1.5
225
+ elif node_type == "datatype" and label in query_datatypes:
226
+ bonus += 1.0
227
+ return min(bonus, 5.0)
228
+
229
+ def _compact_subgraph(self, candidates: list[dict], tool_graph: ToolGraph) -> dict:
230
+ nodes = {}
231
+ edges = []
232
+ candidate_node_ids = {item.get("node_id") for item in candidates if item.get("node_id")}
233
+ server_names = {item.get("server") for item in candidates if item.get("server")}
234
+ for server_name in server_names:
235
+ server_node = f"server:{server_name}"
236
+ if server_node in tool_graph.nodes:
237
+ nodes[server_node] = tool_graph.nodes[server_node]
238
+ for node_id in candidate_node_ids:
239
+ if node_id in tool_graph.nodes:
240
+ nodes[node_id] = tool_graph.nodes[node_id]
241
+ for edge in tool_graph.adjacency.get(node_id, []):
242
+ target = edge.get("target")
243
+ if target in tool_graph.nodes and tool_graph.nodes[target].get("type") in {
244
+ "operation",
245
+ "capability",
246
+ "datatype",
247
+ "stage",
248
+ "constraint",
249
+ }:
250
+ nodes[target] = tool_graph.nodes[target]
251
+ edges.append(edge)
252
+ return {"nodes": list(nodes.values()), "edges": edges[:120]}
253
+
254
+ def _expand_tool_context(self, tool: dict, tool_graph: ToolGraph) -> list[dict]:
255
+ node_id = tool.get("node_id")
256
+ if not node_id:
257
+ return []
258
+ context = []
259
+ for edge in tool_graph.adjacency.get(node_id, [])[:20]:
260
+ target = tool_graph.nodes.get(edge.get("target"), {})
261
+ if target:
262
+ context.append({"edge": edge.get("type"), "node_type": target.get("type"), "label": target.get("label")})
263
+ return context
264
+
265
+ def _operation_path_hint(self, candidates: list[dict]) -> list[str]:
266
+ operations = []
267
+ ordered = sorted(
268
+ candidates,
269
+ key=lambda item: (self.STAGE_RANK.get(item.get("stage"), 9), -float(item.get("score", 0.0))),
270
+ )
271
+ for item in ordered:
272
+ for operation in item.get("operations", []):
273
+ if operation not in operations:
274
+ operations.append(operation)
275
+ return operations[:8]
276
+
277
+ def _format_context(self, candidates: list[dict], compact: dict) -> str:
278
+ lines = [
279
+ f"Retrieved compact MCP graph: {len(compact.get('nodes', []))} nodes, {len(compact.get('edges', []))} edges",
280
+ "Top candidate tool bindings:",
281
+ ]
282
+ for item in candidates[:12]:
283
+ lines.append(
284
+ "- "
285
+ + f"{item['server']}.{item.get('tool')} score={item['score']} "
286
+ + f"stage={item.get('stage')} ops={','.join(item.get('operations', [])[:4])}; {item.get('reason', '')}"
287
+ )
288
+ return "\n".join(lines)
289
+
290
+ def _server_text(self, server_name: str, server_entry: dict, payload: dict) -> str:
291
+ semantics = payload.get("semantics", {})
292
+ return " ".join(
293
+ [
294
+ server_name,
295
+ str(server_entry.get("summary", "")),
296
+ " ".join(server_entry.get("keywords", [])),
297
+ " ".join(semantics.get("keywords", [])),
298
+ " ".join(semantics.get("operations", [])),
299
+ " ".join(semantics.get("capabilities", [])),
300
+ " ".join(semantics.get("datatypes", [])),
301
+ ]
302
+ ).lower()
303
+
304
+ def _tokenize(self, text: str) -> set[str]:
305
+ normalized = (text or "").lower().replace("-", " ").replace("/", " ").replace("_", " ")
306
+ return {token for token in self.TOKEN_PATTERN.findall(normalized) if token}
Biomanus_upload/biomni/graph/graph_router.py ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ from collections import deque
5
+
6
+ from biomni.graph.graph_rag import MCPGraphRAGRetriever
7
+ from biomni.graph.planning_agent import GraphPlanningAgent
8
+ from biomni.graph.schema_extractor import ToolSchemaExtractor
9
+ from biomni.graph.tool_graph import ToolGraph
10
+
11
+
12
+ class GraphRouter:
13
+ """First-pass GraphRAG router over the explicit tool graph."""
14
+
15
+ STAGE_ORDER = ["input_acquisition", "preprocessing", "analysis", "downstream", "reporting"]
16
+
17
+ def __init__(self, schema_extractor: ToolSchemaExtractor | None = None):
18
+ self.schema_extractor = schema_extractor or ToolSchemaExtractor()
19
+ self.planning_agent = GraphPlanningAgent(schema_extractor=self.schema_extractor)
20
+ self.graph_rag = MCPGraphRAGRetriever()
21
+
22
+ def route(
23
+ self,
24
+ query_context: dict,
25
+ tool_graph: ToolGraph,
26
+ *,
27
+ retriever=None,
28
+ llm=None,
29
+ top_k_servers: int = 8,
30
+ candidate_pool: int = 24,
31
+ top_k_tools: int = 12,
32
+ ) -> dict:
33
+ if not tool_graph.get_server_names():
34
+ return {"selected_servers": [], "execution_plan": [], "seed_nodes": [], "workflow_subgraph": []}
35
+
36
+ query_semantics = self.schema_extractor.infer_query_semantics(query_context)
37
+ task_spec = query_semantics.get("task_spec") or {}
38
+ operation_anchors = query_semantics.get("operation_anchors", []) or task_spec.get("required_operations", [])
39
+ graph_rag_result = self.graph_rag.retrieve(
40
+ query_context=query_context,
41
+ query_semantics=query_semantics,
42
+ tool_graph=tool_graph,
43
+ candidate_pool=candidate_pool,
44
+ top_k_tools=top_k_tools,
45
+ )
46
+ if graph_rag_result.get("operation_path_hint"):
47
+ operation_anchors = self._merge_labels(operation_anchors, graph_rag_result["operation_path_hint"])
48
+ seed_nodes = self._find_seed_nodes(query_semantics, tool_graph)
49
+ candidate_paths = self._constrained_path_search(task_spec, operation_anchors, tool_graph)
50
+ ranked_paths = self._rank_paths(candidate_paths, task_spec, operation_anchors)
51
+ best_path = ranked_paths[0] if ranked_paths else self._anchor_only_path(operation_anchors, tool_graph)
52
+ heuristic_tool_plan = self._bind_tools_to_operations(
53
+ best_path,
54
+ task_spec,
55
+ query_semantics,
56
+ query_context,
57
+ tool_graph,
58
+ top_k_tools=top_k_tools,
59
+ )
60
+ planner_result = self.planning_agent.plan(
61
+ query_context=query_context,
62
+ query_semantics=query_semantics,
63
+ task_spec=task_spec,
64
+ ranked_paths=ranked_paths,
65
+ heuristic_tool_plan=heuristic_tool_plan,
66
+ tool_graph=tool_graph,
67
+ top_k_tools=top_k_tools,
68
+ llm=llm,
69
+ graph_rag_result=graph_rag_result,
70
+ )
71
+ tool_plan = planner_result.get("tool_plan") or heuristic_tool_plan
72
+ self._augment_graph_rag_with_tool_plan(graph_rag_result, tool_plan, tool_graph)
73
+ selected_servers = self._servers_from_tool_plan(tool_plan, tool_graph, top_k_servers=top_k_servers)
74
+ execution_plan = self._synthesize_dag(tool_plan)
75
+ operation_plan = self._build_operation_plan(
76
+ task_spec,
77
+ operation_anchors,
78
+ ranked_paths,
79
+ tool_plan,
80
+ planner_result=planner_result,
81
+ )
82
+ workflow_subgraph = [
83
+ {
84
+ "server": server["name"],
85
+ "tools": [tool.get("name", "") for tool in server.get("tools", [])],
86
+ "category": server.get("category", "general"),
87
+ "score": server.get("score", 0.0),
88
+ }
89
+ for server in selected_servers
90
+ ]
91
+ return {
92
+ "task_spec": task_spec,
93
+ "operation_anchors": operation_anchors,
94
+ "candidate_paths": ranked_paths[:5],
95
+ "tool_plan": tool_plan,
96
+ "heuristic_tool_plan": heuristic_tool_plan,
97
+ "dag": execution_plan,
98
+ "heuristic_dag": self._synthesize_dag(heuristic_tool_plan),
99
+ "selected_servers": selected_servers,
100
+ "operation_plan": operation_plan,
101
+ "execution_plan": execution_plan,
102
+ "seed_nodes": seed_nodes,
103
+ "workflow_subgraph": workflow_subgraph,
104
+ "planning_agent": planner_result,
105
+ "graph_rag": graph_rag_result,
106
+ "compact_graph_rag_subgraph": graph_rag_result.get("compact_subgraph", {}),
107
+ "excluded_mcp_servers": graph_rag_result.get("excluded_servers", []),
108
+ }
109
+
110
+ def _merge_labels(self, primary: list[str], additions: list[str]) -> list[str]:
111
+ merged = list(primary or [])
112
+ for item in additions or []:
113
+ if item and item not in merged:
114
+ merged.append(item)
115
+ return merged
116
+
117
+ def _augment_graph_rag_with_tool_plan(self, graph_rag_result: dict, tool_plan: list[dict], tool_graph: ToolGraph) -> None:
118
+ """Ensure the compact GraphRAG context contains the planner-bound graph tools."""
119
+ compact = graph_rag_result.setdefault("compact_subgraph", {"nodes": [], "edges": []})
120
+ nodes = compact.setdefault("nodes", [])
121
+ edges = compact.setdefault("edges", [])
122
+ node_ids = {node.get("id") for node in nodes}
123
+ edge_keys = {(edge.get("source"), edge.get("target"), edge.get("type")) for edge in edges}
124
+
125
+ def add_node(node_id: str) -> None:
126
+ if node_id in node_ids or node_id not in tool_graph.nodes:
127
+ return
128
+ nodes.append(tool_graph.nodes[node_id])
129
+ node_ids.add(node_id)
130
+
131
+ def add_edge(edge: dict) -> None:
132
+ key = (edge.get("source"), edge.get("target"), edge.get("type"))
133
+ if key in edge_keys:
134
+ return
135
+ edges.append(edge)
136
+ edge_keys.add(key)
137
+
138
+ for step in tool_plan:
139
+ if step.get("binding_source") == "agent":
140
+ continue
141
+ server = step.get("server")
142
+ tool = step.get("tool")
143
+ if not server or not tool:
144
+ continue
145
+ server_node = f"server:{server}"
146
+ tool_node = step.get("node_id") or f"tool:{server}:{tool}"
147
+ operation_node = f"operation:{step.get('operation')}"
148
+ add_node(server_node)
149
+ add_node(tool_node)
150
+ add_node(operation_node)
151
+ for edge in tool_graph.adjacency.get(server_node, []):
152
+ if edge.get("target") == tool_node:
153
+ add_edge(edge)
154
+ for edge in tool_graph.adjacency.get(tool_node, []):
155
+ target = edge.get("target")
156
+ if target == operation_node or (
157
+ target in tool_graph.nodes
158
+ and tool_graph.nodes[target].get("type") in {"operation", "capability", "datatype", "stage", "constraint"}
159
+ ):
160
+ add_node(target)
161
+ add_edge(edge)
162
+
163
+ def _find_seed_nodes(self, query_semantics: dict, tool_graph: ToolGraph) -> list[str]:
164
+ seeds = []
165
+ for capability in query_semantics.get("capabilities", []):
166
+ node_id = f"capability:{capability}"
167
+ if node_id in tool_graph.nodes:
168
+ seeds.append(node_id)
169
+ for datatype in query_semantics.get("datatypes", []):
170
+ node_id = f"datatype:{datatype}"
171
+ if node_id in tool_graph.nodes:
172
+ seeds.append(node_id)
173
+ for stage in query_semantics.get("stages", []):
174
+ node_id = f"stage:{stage}"
175
+ if node_id in tool_graph.nodes:
176
+ seeds.append(node_id)
177
+ return seeds
178
+
179
+ def _score_server(self, server_name: str, query_context: dict, query_semantics: dict, tool_graph: ToolGraph) -> dict:
180
+ entry = tool_graph.get_server_entry(server_name) or {}
181
+ semantics = tool_graph.get_server_semantics(server_name)
182
+ score = 0.0
183
+ reasons = []
184
+
185
+ query_keywords = set(query_semantics.get("keywords", []))
186
+ server_keywords = set(semantics.get("keywords", []))
187
+ keyword_overlap = query_keywords & server_keywords
188
+ if keyword_overlap:
189
+ score += min(len(keyword_overlap), 8) * 0.6
190
+ reasons.append(f"keyword overlap={', '.join(sorted(list(keyword_overlap))[:6])}")
191
+
192
+ query_capabilities = set(query_semantics.get("capabilities", []))
193
+ server_capabilities = set(semantics.get("capabilities", []))
194
+ capability_overlap = query_capabilities & server_capabilities
195
+ if capability_overlap:
196
+ score += len(capability_overlap) * 3.0
197
+ reasons.append(f"capabilities={', '.join(sorted(capability_overlap))}")
198
+
199
+ query_datatypes = set(query_semantics.get("datatypes", []))
200
+ server_datatypes = set(semantics.get("datatypes", []))
201
+ datatype_overlap = query_datatypes & server_datatypes
202
+ if datatype_overlap:
203
+ score += len(datatype_overlap) * 2.0
204
+ reasons.append(f"datatypes={', '.join(sorted(datatype_overlap))}")
205
+
206
+ query_stages = set(query_semantics.get("stages", []))
207
+ server_stages = set(semantics.get("stages", []))
208
+ stage_overlap = query_stages & server_stages
209
+ if stage_overlap:
210
+ score += len(stage_overlap) * 1.5
211
+ reasons.append(f"stages={', '.join(sorted(stage_overlap))}")
212
+
213
+ for hint in query_context.get("server_hints", []):
214
+ hint_l = str(hint).lower()
215
+ if self._hint_matches_server(hint_l, server_name, entry):
216
+ score += 8.0
217
+ reasons.append(f"server_hint={hint_l}")
218
+
219
+ category = str(entry.get("category", "")).lower()
220
+ for category_hint in query_context.get("categories", []):
221
+ if str(category_hint).lower() == category:
222
+ score += 2.0
223
+ reasons.append(f"category={category}")
224
+
225
+ tool_bonus = min(len(tool_graph.get_tool_nodes_for_server(server_name)), 6) * 0.15
226
+ score += tool_bonus
227
+
228
+ return {
229
+ "server_name": server_name,
230
+ "server_entry": entry,
231
+ "score": score,
232
+ "reason": reasons,
233
+ }
234
+
235
+ def _select_tools_for_server(
236
+ self,
237
+ server_name: str,
238
+ query_context: dict,
239
+ query_semantics: dict,
240
+ tool_graph: ToolGraph,
241
+ *,
242
+ retriever=None,
243
+ llm=None,
244
+ top_k_tools: int = 12,
245
+ ) -> list[dict]:
246
+ tools = tool_graph.get_tool_nodes_for_server(server_name)
247
+ if not tools:
248
+ return []
249
+
250
+ scored = []
251
+ query_keywords = set(query_semantics.get("keywords", []))
252
+ query_capabilities = set(query_semantics.get("capabilities", []))
253
+ query_datatypes = set(query_semantics.get("datatypes", []))
254
+ query_stages = set(query_semantics.get("stages", []))
255
+
256
+ for tool in tools:
257
+ score = 0.0
258
+ tool_keywords = set(tool.get("keywords", []))
259
+ capability_overlap = query_capabilities & set(tool.get("capabilities", []))
260
+ datatype_overlap = query_datatypes & (set(tool.get("consumes", [])) | set(tool.get("produces", [])))
261
+ stage_overlap = query_stages & {tool.get("stage")} if tool.get("stage") else set()
262
+
263
+ keyword_overlap = query_keywords & tool_keywords
264
+ score += min(len(keyword_overlap), 6) * 0.5
265
+ score += len(capability_overlap) * 3.0
266
+ score += len(datatype_overlap) * 2.0
267
+ score += len(stage_overlap) * 1.0
268
+
269
+ for hint in query_context.get("server_hints", []):
270
+ hint_l = str(hint).lower()
271
+ if self._hint_matches_tool(hint_l, tool):
272
+ score += 5.0
273
+ scored.append((score, tool))
274
+
275
+ scored.sort(key=lambda item: item[0], reverse=True)
276
+ shortlisted = [self._tool_to_runtime_schema(tool) for _, tool in scored[: max(top_k_tools * 2, top_k_tools)]]
277
+
278
+ if retriever is not None and llm is not None and len(shortlisted) > top_k_tools:
279
+ reranked = retriever._llm_rerank_tools(query_context, shortlisted, top_k=top_k_tools, llm=llm)
280
+ if reranked:
281
+ return reranked
282
+ return shortlisted[:top_k_tools]
283
+
284
+ def _hint_matches_server(self, hint: str, server_name: str, entry: dict) -> bool:
285
+ if len(hint) < 3:
286
+ return False
287
+ server_l = server_name.lower()
288
+ if hint == server_l or hint in server_l.split("-") or hint in server_l.split("_"):
289
+ return True
290
+ tokens = set(str(entry.get("summary", "")).lower().replace("_", "-").split())
291
+ return hint in tokens
292
+
293
+ def _hint_matches_tool(self, hint: str, tool: dict) -> bool:
294
+ if len(hint) < 3:
295
+ return False
296
+ tool_name = str(tool.get("name", "")).lower()
297
+ if hint == tool_name or hint in tool_name.replace("_", "-").split("-"):
298
+ return True
299
+ tokens = set(str(tool.get("text", "")).lower().replace("_", "-").split())
300
+ return hint in tokens
301
+
302
+ def _tool_to_runtime_schema(self, tool: dict) -> dict:
303
+ return {
304
+ "name": tool.get("name"),
305
+ "description": tool.get("description", ""),
306
+ "inputSchema": tool.get("inputSchema", {"properties": {}, "required": []}),
307
+ }
308
+
309
+ def _constrained_path_search(
310
+ self,
311
+ task_spec: dict,
312
+ operation_anchors: list[str],
313
+ tool_graph: ToolGraph,
314
+ *,
315
+ max_hops: int = 8,
316
+ beam_width: int = 80,
317
+ ) -> list[dict]:
318
+ source_types = task_spec.get("input_types", []) or ["text"]
319
+ target_type = (task_spec.get("target_output") or {}).get("semantic_type")
320
+ constraint_labels = set((task_spec.get("constraints") or {}).get("labels", []))
321
+ reachable_anchors = [anchor for anchor in operation_anchors if anchor in tool_graph.operation_index]
322
+ paths = []
323
+ queue = deque((source_type, [], [source_type]) for source_type in source_types)
324
+ visited = set()
325
+
326
+ while queue and len(paths) < beam_width:
327
+ current_type, operations, type_trace = queue.popleft()
328
+ state = (current_type, tuple(operations))
329
+ if state in visited:
330
+ continue
331
+ visited.add(state)
332
+
333
+ if operations and (not target_type or current_type == target_type):
334
+ if self._path_satisfies_anchors(operations, reachable_anchors):
335
+ paths.append(self._path_payload(operations, type_trace, target_type, tool_graph))
336
+ continue
337
+
338
+ if len(operations) >= max_hops:
339
+ continue
340
+
341
+ for operation in tool_graph.datatype_to_operations.get(current_type, []):
342
+ if operation in operations:
343
+ continue
344
+ operation_info = tool_graph.operation_index.get(operation, {})
345
+ if not self._constraints_compatible(operation_info.get("constraints", []), constraint_labels):
346
+ continue
347
+ for produced_type in operation_info.get("produces", []) or []:
348
+ queue.append((produced_type, [*operations, operation], [*type_trace, produced_type]))
349
+
350
+ if not paths and reachable_anchors:
351
+ return [self._anchor_only_path(reachable_anchors, tool_graph)]
352
+ return paths
353
+
354
+ def _path_satisfies_anchors(self, operations: list[str], anchors: list[str]) -> bool:
355
+ return all(anchor in operations for anchor in anchors)
356
+
357
+ def _constraints_compatible(self, required: list[str], provided: set[str]) -> bool:
358
+ # Phase 1 routing is intentionally driven by the semantic type graph.
359
+ # Constraints are kept on operation/tool nodes for ranking and prompt
360
+ # explanation, but should not prune a path that can satisfy them via an
361
+ # upstream operation (for example gene_id_conversion -> kegg_enrichment).
362
+ return True
363
+
364
+ def _path_payload(
365
+ self,
366
+ operations: list[str],
367
+ type_trace: list[str],
368
+ target_type: str | None,
369
+ tool_graph: ToolGraph,
370
+ ) -> dict:
371
+ steps = []
372
+ for index, operation in enumerate(operations):
373
+ operation_info = tool_graph.operation_index.get(operation, {})
374
+ steps.append(
375
+ {
376
+ "operation": operation,
377
+ "input_type": type_trace[index],
378
+ "output_types": operation_info.get("produces", []),
379
+ "stage": operation_info.get("stage", "analysis"),
380
+ }
381
+ )
382
+ return {
383
+ "operations": operations,
384
+ "type_trace": type_trace,
385
+ "target_type": target_type,
386
+ "steps": steps,
387
+ }
388
+
389
+ def _anchor_only_path(self, anchors: list[str], tool_graph: ToolGraph) -> dict:
390
+ operations = [operation for operation in anchors if operation in tool_graph.operation_index]
391
+ if not operations:
392
+ operations = list(tool_graph.operation_index.keys())[:1]
393
+ type_trace = []
394
+ for operation in operations:
395
+ operation_info = tool_graph.operation_index.get(operation, {})
396
+ if not type_trace:
397
+ type_trace.append((operation_info.get("accepts") or ["text"])[0])
398
+ type_trace.append((operation_info.get("produces") or [type_trace[-1]])[0])
399
+ return self._path_payload(operations, type_trace or ["text"], None, tool_graph)
400
+
401
+ def _rank_paths(self, paths: list[dict], task_spec: dict, anchors: list[str]) -> list[dict]:
402
+ target_type = (task_spec.get("target_output") or {}).get("semantic_type")
403
+ ranked = []
404
+ for path in paths:
405
+ operations = path.get("operations", [])
406
+ anchor_hits = len(set(operations) & set(anchors))
407
+ target_hit = 1 if target_type and target_type in path.get("type_trace", []) else 0
408
+ score = anchor_hits * 5.0 + target_hit * 3.0 - len(operations) * 0.2
409
+ enriched = dict(path)
410
+ enriched["score"] = score
411
+ enriched["anchor_hits"] = anchor_hits
412
+ ranked.append(enriched)
413
+ ranked.sort(key=lambda item: item.get("score", 0), reverse=True)
414
+ return ranked
415
+
416
+ def _bind_tools_to_operations(
417
+ self,
418
+ path: dict,
419
+ task_spec: dict,
420
+ query_semantics: dict,
421
+ query_context: dict,
422
+ tool_graph: ToolGraph,
423
+ *,
424
+ top_k_tools: int = 12,
425
+ ) -> list[dict]:
426
+ constraint_labels = set((task_spec.get("constraints") or {}).get("labels", []))
427
+ query_keywords = {
428
+ keyword
429
+ for keyword in query_semantics.get("keywords", [])
430
+ if isinstance(keyword, str) and len(keyword) >= 3
431
+ }
432
+ generic_query_terms = {
433
+ "according",
434
+ "against",
435
+ "annotation",
436
+ "answer",
437
+ "biological",
438
+ "but",
439
+ "choices",
440
+ "data",
441
+ "database",
442
+ "entry",
443
+ "following",
444
+ "gene",
445
+ "genes",
446
+ "identify",
447
+ "knowledge",
448
+ "metadata",
449
+ "not",
450
+ "query",
451
+ "record",
452
+ "referenced",
453
+ "relevant",
454
+ "retrieve",
455
+ "retrieved",
456
+ "set",
457
+ "source",
458
+ "task",
459
+ "which",
460
+ }
461
+ discriminative_keywords = query_keywords - generic_query_terms
462
+ query_categories = {str(category).lower() for category in query_context.get("categories", [])}
463
+ tool_plan = []
464
+ for step_index, operation in enumerate(path.get("operations", []), start=1):
465
+ operation_info = tool_graph.operation_index.get(operation, {})
466
+ candidates = []
467
+ for tool in operation_info.get("tools", []):
468
+ server_name = tool.get("node_id", "tool:unknown:unknown").split(":")[1]
469
+ server_entry = tool_graph.get_server_entry(server_name) or {}
470
+ tool_constraints = set(tool.get("constraints", []))
471
+ tool_text = " ".join(
472
+ [
473
+ server_name,
474
+ str(server_entry.get("summary", "")),
475
+ str(tool.get("name", "")),
476
+ str(tool.get("description", "")),
477
+ " ".join(tool.get("keywords", [])),
478
+ ]
479
+ ).lower()
480
+ operation_spec = self.schema_extractor.OPERATION_SPECS.get(operation, {})
481
+ operation_patterns = operation_spec.get("patterns", set())
482
+ score = 1.0
483
+ score += len(tool_constraints & constraint_labels) * 2.0
484
+ score += min(len(tool.get("keywords", [])), 8) * 0.05
485
+ keyword_hits = {keyword for keyword in query_keywords if keyword in tool_text}
486
+ discriminative_hits = {keyword for keyword in discriminative_keywords if keyword in tool_text}
487
+ score += min(len(keyword_hits), 10) * 0.45
488
+ score += min(len(discriminative_hits), 6) * 1.2
489
+ category = str(server_entry.get("category", "")).lower()
490
+ if category and category in query_categories:
491
+ score += 1.0
492
+ if operation in tool.get("name", "").lower():
493
+ score += 1.0
494
+ if operation.replace("_", " ") in tool_text:
495
+ score += 1.5
496
+ score += min(sum(1 for pattern in operation_patterns if pattern in tool_text), 3) * 0.8
497
+ if operation == "csv_export":
498
+ tool_name = str(tool.get("name", "")).lower()
499
+ if "export" in tool_name:
500
+ score += 2.0
501
+ if "csv" in tool_name:
502
+ score += 1.0
503
+ if tool_name.startswith(("read", "load", "import", "fetch", "get")) or "_read_" in tool_name:
504
+ score -= 3.0
505
+ candidates.append((score, tool.get("node_id", ""), tool))
506
+ candidates.sort(key=lambda item: (item[0], item[1]), reverse=True)
507
+ selected = candidates[0][2] if candidates else {}
508
+ server_name = selected.get("node_id", "tool:unknown:unknown").split(":")[1]
509
+ tool_plan.append(
510
+ {
511
+ "step": step_index,
512
+ "operation": operation,
513
+ "server": server_name,
514
+ "tool": selected.get("name"),
515
+ "stage": operation_info.get("stage", "analysis"),
516
+ "input_types": operation_info.get("accepts", []),
517
+ "output_types": operation_info.get("produces", []),
518
+ "candidate_tool_count": len(candidates),
519
+ }
520
+ )
521
+ return tool_plan[:top_k_tools]
522
+
523
+ def _servers_from_tool_plan(self, tool_plan: list[dict], tool_graph: ToolGraph, *, top_k_servers: int) -> list[dict]:
524
+ grouped: dict[str, list[dict]] = {}
525
+ for step in tool_plan:
526
+ if step.get("binding_source") == "agent":
527
+ continue
528
+ if step.get("server") and step.get("tool"):
529
+ grouped.setdefault(step["server"], []).append(step)
530
+
531
+ selected_servers = []
532
+ for server_name, steps in list(grouped.items())[:top_k_servers]:
533
+ entry = copy.deepcopy(tool_graph.get_server_entry(server_name) or {"name": server_name})
534
+ entry["tools"] = [
535
+ {
536
+ "name": step["tool"],
537
+ "description": f"Tool bound for operation {step['operation']}",
538
+ "inputSchema": {"properties": {}, "required": []},
539
+ }
540
+ for step in steps
541
+ ]
542
+ entry["score"] = float(len(steps))
543
+ entry["graph_reason"] = [f"operation_path_binding={step['operation']}" for step in steps]
544
+ selected_servers.append(entry)
545
+ return selected_servers
546
+
547
+ def _synthesize_dag(self, tool_plan: list[dict]) -> list[dict]:
548
+ dag = []
549
+ previous_id = None
550
+ seen_ids: dict[str, int] = {}
551
+ for index, step in enumerate(tool_plan, start=1):
552
+ base_id = f"{step['server']}.{step['tool'] or step['operation']}"
553
+ seen_ids[base_id] = seen_ids.get(base_id, 0) + 1
554
+ suffix = "" if seen_ids[base_id] == 1 else f"#{seen_ids[base_id]}"
555
+ step_id = f"{base_id}{suffix}"
556
+ dag.append(
557
+ {
558
+ "id": step_id,
559
+ "step": index,
560
+ "stage": step.get("stage", "analysis"),
561
+ "operation": step.get("operation"),
562
+ "server": step.get("server"),
563
+ "tools": [step.get("tool") or step.get("operation")],
564
+ "input_types": step.get("input_types", []),
565
+ "output_types": step.get("output_types", []),
566
+ "binding_source": step.get("binding_source", "graph"),
567
+ "depends_on": [previous_id] if previous_id else [],
568
+ }
569
+ )
570
+ previous_id = step_id
571
+ return dag
572
+
573
+ def _build_operation_plan(
574
+ self,
575
+ task_spec: dict,
576
+ operation_anchors: list[str],
577
+ ranked_paths: list[dict],
578
+ tool_plan: list[dict],
579
+ *,
580
+ planner_result: dict | None = None,
581
+ ) -> dict:
582
+ best_path = ranked_paths[0] if ranked_paths else {}
583
+ selected_path = planner_result.get("operation_path") if planner_result else None
584
+ selected_trace = planner_result.get("type_trace") if planner_result else None
585
+ return {
586
+ "workflow_type": task_spec.get("workflow_type", "single_step"),
587
+ "planner": (planner_result or {}).get("planner", "heuristic_graph_router"),
588
+ "planner_selected": bool((planner_result or {}).get("selected")),
589
+ "task_spec": task_spec,
590
+ "operation_anchors": operation_anchors,
591
+ "operation_path": selected_path or best_path.get("operations", []),
592
+ "type_trace": selected_trace or best_path.get("type_trace", []),
593
+ "steps": [
594
+ {
595
+ "operation": step.get("operation"),
596
+ "server": step.get("server"),
597
+ "tool": step.get("tool"),
598
+ "input_types": step.get("input_types", []),
599
+ "output_types": step.get("output_types", []),
600
+ "binding_source": step.get("binding_source", "graph"),
601
+ "planning_rationale": step.get("planning_rationale", ""),
602
+ }
603
+ for step in tool_plan
604
+ ],
605
+ "tool_bindings": [
606
+ {
607
+ "operation": step.get("operation"),
608
+ "server": step.get("server"),
609
+ "tool": step.get("tool"),
610
+ "binding_source": step.get("binding_source", "graph"),
611
+ }
612
+ for step in tool_plan
613
+ ],
614
+ "planner_reason": (planner_result or {}).get("reason", ""),
615
+ "planner_task_profile": (planner_result or {}).get("task_profile", {}),
616
+ }
617
+
618
+ def _build_execution_plan(self, selected_servers: list[dict], tool_graph: ToolGraph) -> list[dict]:
619
+ selected_nodes = {}
620
+ for server in selected_servers:
621
+ selected_tool_names = {candidate.get("name") for candidate in server.get("tools", [])}
622
+ for tool in tool_graph.get_tool_nodes_for_server(server["name"]):
623
+ if tool.get("name") in selected_tool_names:
624
+ selected_nodes[tool["node_id"]] = tool
625
+
626
+ if selected_nodes:
627
+ dependency_map: dict[str, list[str]] = {node_id: [] for node_id in selected_nodes}
628
+ for edge in tool_graph.edges:
629
+ if edge.get("type") not in {"typed_flow", "follows"}:
630
+ continue
631
+ source = edge.get("source")
632
+ target = edge.get("target")
633
+ if source in selected_nodes and target in selected_nodes:
634
+ dependency_map[target].append(source)
635
+
636
+ stage_rank = {stage: idx for idx, stage in enumerate(self.STAGE_ORDER)}
637
+ ordered_nodes = sorted(
638
+ selected_nodes.values(),
639
+ key=lambda tool: (
640
+ stage_rank.get(tool.get("stage") or "analysis", 99),
641
+ len(dependency_map.get(tool["node_id"], [])),
642
+ tool["node_id"].split(":")[1],
643
+ tool.get("name", ""),
644
+ ),
645
+ )
646
+ plan = []
647
+ for tool in ordered_nodes:
648
+ server = tool["node_id"].split(":")[1]
649
+ depends_on = [
650
+ f"{selected_nodes[source]['node_id'].split(':')[1]}.{selected_nodes[source].get('name')}"
651
+ for source in dependency_map.get(tool["node_id"], [])
652
+ ]
653
+ plan.append(
654
+ {
655
+ "stage": tool.get("stage") or "analysis",
656
+ "server": server,
657
+ "tools": [tool.get("name")],
658
+ "depends_on": depends_on,
659
+ }
660
+ )
661
+ return plan
662
+
663
+ plan = []
664
+ for server in selected_servers:
665
+ stage_buckets: dict[str, list[str]] = {}
666
+ for tool in tool_graph.get_tool_nodes_for_server(server["name"]):
667
+ if tool.get("name") not in {candidate.get("name") for candidate in server.get("tools", [])}:
668
+ continue
669
+ stage = tool.get("stage") or "analysis"
670
+ stage_buckets.setdefault(stage, []).append(tool.get("name"))
671
+ for stage in self.STAGE_ORDER:
672
+ if stage in stage_buckets:
673
+ plan.append({"stage": stage, "server": server["name"], "tools": sorted(stage_buckets[stage])})
674
+ return plan
Biomanus_upload/biomni/graph/planning_agent.py ADDED
@@ -0,0 +1,1046 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+
6
+ from langchain_core.messages import HumanMessage
7
+
8
+ from biomni.graph.schema_extractor import ToolSchemaExtractor
9
+ from biomni.graph.tool_graph import ToolGraph
10
+
11
+
12
+ class GraphPlanningAgent:
13
+ """Task-aware planner that refines graph routing into a more usable call chain."""
14
+
15
+ GENERIC_TOKENS = {
16
+ "according",
17
+ "analysis",
18
+ "annotate",
19
+ "answer",
20
+ "based",
21
+ "biological",
22
+ "candidate",
23
+ "choice",
24
+ "choices",
25
+ "compare",
26
+ "comparison",
27
+ "context",
28
+ "data",
29
+ "database",
30
+ "evidence",
31
+ "following",
32
+ "identify",
33
+ "likely",
34
+ "most",
35
+ "prompt",
36
+ "question",
37
+ "record",
38
+ "relevant",
39
+ "result",
40
+ "sequence",
41
+ "source",
42
+ "summary",
43
+ "task",
44
+ "tool",
45
+ "using",
46
+ "variant",
47
+ "which",
48
+ }
49
+ AMINO_ACID_SEQUENCE = re.compile(r"[ACDEFGHIKLMNPQRSTVWY]{12,}")
50
+ TOKEN_PATTERN = re.compile(r"[a-z0-9_+-]+")
51
+ EDGE_TYPES = {"typed_flow", "follows", "adjacent_stage"}
52
+ RESOURCE_TOKENS = {
53
+ "clinvar",
54
+ "disgenet",
55
+ "omim",
56
+ "uniprot",
57
+ "ensembl",
58
+ "pubmed",
59
+ "biomart",
60
+ }
61
+ SEQUENCING_INPUT_TYPES = {"fastq", "bam", "reference_genome", "annotation_gff", "raw_count_matrix", "count_matrix"}
62
+ WRONG_DOMAIN_TOKENS = {
63
+ "crispr",
64
+ "amplicon",
65
+ "guide",
66
+ "single-cell",
67
+ "singlecell",
68
+ "spatial",
69
+ "metagenomic",
70
+ "metagenomics",
71
+ "taxonomy",
72
+ "taxonomic",
73
+ "promoter",
74
+ "orf",
75
+ }
76
+
77
+ def __init__(self, schema_extractor: ToolSchemaExtractor | None = None):
78
+ self.schema_extractor = schema_extractor or ToolSchemaExtractor()
79
+
80
+ def plan(
81
+ self,
82
+ *,
83
+ query_context: dict,
84
+ query_semantics: dict,
85
+ task_spec: dict,
86
+ ranked_paths: list[dict],
87
+ heuristic_tool_plan: list[dict],
88
+ tool_graph: ToolGraph,
89
+ top_k_tools: int = 12,
90
+ llm=None,
91
+ graph_rag_result: dict | None = None,
92
+ ) -> dict:
93
+ profile = self._build_task_profile(query_context, query_semantics, task_spec)
94
+ if graph_rag_result:
95
+ profile["graph_rag"] = graph_rag_result.get("task_profile", {})
96
+ profile["task_tags"] = sorted(
97
+ set(profile.get("task_tags", [])) | set((graph_rag_result.get("task_profile") or {}).get("tags", []))
98
+ )
99
+ step_specs = self._build_step_specs(query_context, task_spec, ranked_paths, profile)
100
+ if llm is not None and graph_rag_result:
101
+ llm_plan = self._plan_with_llm(
102
+ llm=llm,
103
+ query_context=query_context,
104
+ query_semantics=query_semantics,
105
+ task_spec=task_spec,
106
+ graph_rag_result=graph_rag_result,
107
+ tool_graph=tool_graph,
108
+ top_k_tools=top_k_tools,
109
+ )
110
+ if llm_plan.get("tool_plan"):
111
+ return {
112
+ "planner": "llm_graph_rag_planning_agent",
113
+ "selected": True,
114
+ "task_profile": profile,
115
+ "step_specs": step_specs,
116
+ "llm_enabled": True,
117
+ "operation_path": [step["operation"] for step in llm_plan["tool_plan"]],
118
+ "type_trace": self._build_type_trace(llm_plan["tool_plan"], task_spec),
119
+ "tool_plan": llm_plan["tool_plan"][:top_k_tools],
120
+ "reason": llm_plan.get("reason", "LLM planned over compact MCP GraphRAG context"),
121
+ "llm_raw_plan": llm_plan.get("raw_plan", {}),
122
+ "heuristic_tool_plan": heuristic_tool_plan,
123
+ "graph_rag": graph_rag_result or {},
124
+ }
125
+
126
+ tool_plan = self._select_tool_chain(
127
+ step_specs,
128
+ profile,
129
+ tool_graph,
130
+ top_k_tools=top_k_tools,
131
+ graph_rag_candidates=(graph_rag_result or {}).get("candidate_tools", []),
132
+ )
133
+ if not tool_plan:
134
+ return {
135
+ "planner": "graph_planning_agent",
136
+ "selected": False,
137
+ "task_profile": profile,
138
+ "step_specs": step_specs,
139
+ "reason": "no viable planner tool chain found; using heuristic route",
140
+ "llm_enabled": llm is not None,
141
+ "graph_rag": graph_rag_result or {},
142
+ }
143
+
144
+ return {
145
+ "planner": "graph_planning_agent",
146
+ "selected": True,
147
+ "task_profile": profile,
148
+ "step_specs": step_specs,
149
+ "llm_enabled": llm is not None,
150
+ "operation_path": [step["operation"] for step in tool_plan],
151
+ "type_trace": self._build_type_trace(tool_plan, task_spec),
152
+ "tool_plan": tool_plan[:top_k_tools],
153
+ "reason": "planner selected a task-grounded tool chain from graph candidates",
154
+ "heuristic_tool_plan": heuristic_tool_plan,
155
+ "graph_rag": graph_rag_result or {},
156
+ }
157
+
158
+ def _plan_with_llm(
159
+ self,
160
+ *,
161
+ llm,
162
+ query_context: dict,
163
+ query_semantics: dict,
164
+ task_spec: dict,
165
+ graph_rag_result: dict,
166
+ tool_graph: ToolGraph,
167
+ top_k_tools: int,
168
+ ) -> dict:
169
+ candidates = graph_rag_result.get("candidate_tools", [])[: max(top_k_tools * 2, top_k_tools)]
170
+ if not candidates:
171
+ return {}
172
+
173
+ prompt = self._build_llm_planner_prompt(
174
+ query_context=query_context,
175
+ query_semantics=query_semantics,
176
+ task_spec=task_spec,
177
+ graph_rag_result=graph_rag_result,
178
+ candidates=candidates,
179
+ top_k_tools=top_k_tools,
180
+ )
181
+ try:
182
+ response = llm.invoke([HumanMessage(content=prompt)])
183
+ except Exception as exc:
184
+ return {"error": f"llm_planner_failed: {type(exc).__name__}: {exc}"}
185
+
186
+ content = getattr(response, "content", response)
187
+ content_text = self._content_to_text(content)
188
+ parsed = self._parse_json_object(content_text)
189
+ if not parsed:
190
+ return {"error": "llm_planner_returned_unparseable_json", "content": content_text[:2000]}
191
+
192
+ tool_plan = self._tool_plan_from_llm_json(parsed, candidates, tool_graph, top_k_tools=top_k_tools)
193
+ if not tool_plan:
194
+ return {"raw_plan": parsed, "reason": parsed.get("reason", "")}
195
+ return {"tool_plan": tool_plan, "raw_plan": parsed, "reason": parsed.get("reason", "")}
196
+
197
+ def _build_llm_planner_prompt(
198
+ self,
199
+ *,
200
+ query_context: dict,
201
+ query_semantics: dict,
202
+ task_spec: dict,
203
+ graph_rag_result: dict,
204
+ candidates: list[dict],
205
+ top_k_tools: int,
206
+ ) -> str:
207
+ compact_candidates = []
208
+ for idx, item in enumerate(candidates):
209
+ compact_candidates.append(
210
+ {
211
+ "index": idx,
212
+ "server": item.get("server"),
213
+ "tool": item.get("tool"),
214
+ "stage": item.get("stage"),
215
+ "operations": item.get("operations", []),
216
+ "capabilities": item.get("capabilities", []),
217
+ "consumes": item.get("consumes", []),
218
+ "produces": item.get("produces", []),
219
+ "score": item.get("score"),
220
+ "graph_context": item.get("graph_context", [])[:8],
221
+ "reason": item.get("reason", ""),
222
+ }
223
+ )
224
+ question = query_context.get("question") or query_context.get("original_query") or query_context.get("rewritten_query")
225
+ payload = {
226
+ "question": question,
227
+ "query_semantics": {
228
+ "operations": query_semantics.get("operations", []),
229
+ "capabilities": query_semantics.get("capabilities", []),
230
+ "datatypes": query_semantics.get("datatypes", []),
231
+ "stages": query_semantics.get("stages", []),
232
+ },
233
+ "task_spec": task_spec,
234
+ "graph_rag": {
235
+ "operation_path_hint": graph_rag_result.get("operation_path_hint", []),
236
+ "compact_node_count": len((graph_rag_result.get("compact_subgraph") or {}).get("nodes", [])),
237
+ "compact_edge_count": len((graph_rag_result.get("compact_subgraph") or {}).get("edges", [])),
238
+ },
239
+ "candidate_tools": compact_candidates,
240
+ }
241
+ return (
242
+ "You are an agentic planner for a biomedical MCP tool graph. "
243
+ "Plan only from the provided GraphRAG candidate tools. "
244
+ "Choose a concise executable operation path. Use an agent reasoning step only when no candidate tool is suitable. "
245
+ "For exact sequence-analysis tasks such as GC content, ORF finding, restriction digests, PCR primers, "
246
+ "or Kozak scoring, prefer general sequence operation tools over broad NGS or database tools when present. "
247
+ "Do not solve the biology task; only plan tool use.\n\n"
248
+ "Return exactly one JSON object with this schema:\n"
249
+ "{\n"
250
+ ' "reason": "short rationale",\n'
251
+ ' "steps": [\n'
252
+ ' {"operation": "operation_name", "candidate_index": 0, "server": "server", "tool": "tool", "rationale": "why"},\n'
253
+ ' {"operation": "option_comparison", "provider": "agent", "tool": "reason_over_evidence", "rationale": "why"}\n'
254
+ " ]\n"
255
+ "}\n"
256
+ f"Use at most {top_k_tools} steps. candidate_index must refer to candidate_tools when using an MCP tool.\n\n"
257
+ "Planning input:\n"
258
+ + json.dumps(payload, ensure_ascii=False, indent=2, default=str)
259
+ )
260
+
261
+ def _tool_plan_from_llm_json(
262
+ self,
263
+ parsed: dict,
264
+ candidates: list[dict],
265
+ tool_graph: ToolGraph,
266
+ *,
267
+ top_k_tools: int,
268
+ ) -> list[dict]:
269
+ plan = []
270
+ candidate_by_index = {idx: item for idx, item in enumerate(candidates)}
271
+ candidate_by_pair = {(item.get("server"), item.get("tool")): item for item in candidates}
272
+ seen = set()
273
+ steps = parsed.get("steps", [])
274
+ if not isinstance(steps, list):
275
+ return []
276
+
277
+ for raw_step in steps[:top_k_tools]:
278
+ if not isinstance(raw_step, dict):
279
+ continue
280
+ operation = str(raw_step.get("operation") or raw_step.get("step") or "").strip()
281
+ if not operation:
282
+ continue
283
+ provider = str(raw_step.get("provider") or raw_step.get("server") or "").strip()
284
+ tool_name = str(raw_step.get("tool") or "").strip()
285
+ candidate = None
286
+ raw_index = raw_step.get("candidate_index")
287
+ if isinstance(raw_index, int):
288
+ candidate = candidate_by_index.get(raw_index)
289
+ if candidate is None and provider and tool_name:
290
+ candidate = candidate_by_pair.get((provider, tool_name))
291
+
292
+ if provider == "agent" or tool_name == "reason_over_evidence" or candidate is None:
293
+ key = ("agent", operation)
294
+ if key in seen:
295
+ continue
296
+ seen.add(key)
297
+ plan.append(
298
+ {
299
+ "step": len(plan) + 1,
300
+ "operation": operation,
301
+ "server": "agent",
302
+ "tool": "reason_over_evidence",
303
+ "stage": raw_step.get("stage") or "downstream",
304
+ "input_types": raw_step.get("input_types", ["text"]),
305
+ "output_types": raw_step.get("output_types", ["report_table"]),
306
+ "candidate_tool_count": len(candidates),
307
+ "binding_source": "agent",
308
+ "planning_rationale": raw_step.get("rationale", ""),
309
+ }
310
+ )
311
+ continue
312
+
313
+ node_id = candidate.get("node_id")
314
+ if node_id and node_id not in tool_graph.nodes:
315
+ continue
316
+ key = (candidate.get("server"), candidate.get("tool"), operation)
317
+ if key in seen:
318
+ continue
319
+ seen.add(key)
320
+ plan.append(
321
+ {
322
+ "step": len(plan) + 1,
323
+ "operation": operation,
324
+ "server": candidate.get("server"),
325
+ "tool": candidate.get("tool"),
326
+ "stage": candidate.get("stage") or raw_step.get("stage") or "analysis",
327
+ "input_types": candidate.get("consumes", []),
328
+ "output_types": candidate.get("produces", []),
329
+ "candidate_tool_count": len(candidates),
330
+ "binding_source": "llm_graph_rag",
331
+ "node_id": node_id,
332
+ "planning_score": candidate.get("score", 0.0),
333
+ "planning_rationale": raw_step.get("rationale", ""),
334
+ }
335
+ )
336
+ return plan
337
+
338
+ def _content_to_text(self, content) -> str:
339
+ if isinstance(content, list):
340
+ parts = []
341
+ for item in content:
342
+ if isinstance(item, dict) and "text" in item:
343
+ parts.append(str(item.get("text", "")))
344
+ else:
345
+ parts.append(str(item))
346
+ return "\n".join(parts)
347
+ return str(content)
348
+
349
+ def _parse_json_object(self, text: str) -> dict:
350
+ try:
351
+ payload = json.loads(text)
352
+ return payload if isinstance(payload, dict) else {}
353
+ except json.JSONDecodeError:
354
+ pass
355
+ match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.DOTALL)
356
+ if match:
357
+ try:
358
+ payload = json.loads(match.group(1))
359
+ return payload if isinstance(payload, dict) else {}
360
+ except json.JSONDecodeError:
361
+ pass
362
+ start = text.find("{")
363
+ end = text.rfind("}")
364
+ if start >= 0 and end > start:
365
+ try:
366
+ payload = json.loads(text[start : end + 1])
367
+ return payload if isinstance(payload, dict) else {}
368
+ except json.JSONDecodeError:
369
+ return {}
370
+ return {}
371
+
372
+ def _build_task_profile(self, query_context: dict, query_semantics: dict, task_spec: dict) -> dict:
373
+ text_parts = [
374
+ query_context.get("question", ""),
375
+ query_context.get("original_query", ""),
376
+ query_context.get("rewritten_query", ""),
377
+ query_context.get("retrieval_query", ""),
378
+ " ".join(query_context.get("subtasks", [])),
379
+ " ".join(query_context.get("keywords", [])),
380
+ ]
381
+ text = " ".join(str(part) for part in text_parts if part).lower()
382
+ query_tokens = self._tokenize(text)
383
+ focus_tokens = {
384
+ token
385
+ for token in query_tokens
386
+ if len(token) >= 4 and token not in self.GENERIC_TOKENS and token not in {"text", "table", "report"}
387
+ }
388
+ resource_tokens = sorted(token for token in self.RESOURCE_TOKENS if token in text)
389
+ question = str(query_context.get("question", "") or query_context.get("original_query", ""))
390
+ categories = {str(item).lower() for item in query_context.get("categories", [])}
391
+ task_tags = self._infer_task_tags(text, categories)
392
+ return {
393
+ "text": text,
394
+ "query_tokens": sorted(query_tokens),
395
+ "focus_tokens": sorted(focus_tokens),
396
+ "resource_tokens": resource_tokens,
397
+ "categories": sorted(categories),
398
+ "has_sequence": "<sequence>" in question.lower() or bool(self.AMINO_ACID_SEQUENCE.search(question.upper())),
399
+ "has_variant": any(token in text for token in ("variant", "mutation", "amino-acid", "amino acid", "missense")),
400
+ "has_gene": any(token in text for token in ("gene", "genes")),
401
+ "needs_lookup": bool(resource_tokens) or "database_question_answering" in categories,
402
+ "needs_comparison": any(
403
+ token in text for token in ("which of the following", "compare", "comparison", "but not", "not according")
404
+ ),
405
+ "is_multiple_choice": "which of the following" in text or "choices" in text,
406
+ "asks_clinical_significance": any(token in text for token in ("benign", "pathogenic", "significance")),
407
+ "task_output": (task_spec.get("target_output") or {}).get("semantic_type") or "report_table",
408
+ "semantic_ops": query_semantics.get("operations", []),
409
+ "task_tags": sorted(task_tags),
410
+ "is_seqqa": "seqqa" in task_tags,
411
+ }
412
+
413
+ def _build_step_specs(self, query_context: dict, task_spec: dict, ranked_paths: list[dict], profile: dict) -> list[dict]:
414
+ steps = []
415
+ task_tags = set(profile.get("task_tags", []))
416
+
417
+ if "base_counting" in task_tags:
418
+ steps.append(
419
+ self._step_spec(
420
+ "base_counting",
421
+ "Count nucleotide bases in the provided sequence using a deterministic sequence tool.",
422
+ stage="analysis",
423
+ tokens=["base", "bases", "count", "sequence", "gc", "composition", "seqfu", "bioawk"],
424
+ inputs=["text", "sequence_feature"],
425
+ outputs=["sequence_feature", "report_table"],
426
+ )
427
+ )
428
+ if "gc_content_calculation" in task_tags:
429
+ steps.append(
430
+ self._step_spec(
431
+ "gc_content_calculation",
432
+ "Compute GC percentage and rounding deterministically before mapping to the choices.",
433
+ stage="analysis",
434
+ tokens=["gc", "content", "percent", "percentage", "base", "count", "composition"],
435
+ inputs=["text", "sequence_feature"],
436
+ outputs=["report_table"],
437
+ )
438
+ )
439
+ if "reverse_complement" in task_tags:
440
+ steps.append(
441
+ self._step_spec(
442
+ "reverse_complement",
443
+ "Compute reverse-complement sequence coordinates when strand direction matters.",
444
+ stage="analysis",
445
+ tokens=["reverse", "complement", "revcomp", "rc", "sequence"],
446
+ inputs=["text", "sequence_feature"],
447
+ outputs=["sequence_feature"],
448
+ )
449
+ )
450
+ if "orf_detection" in task_tags:
451
+ steps.append(
452
+ self._step_spec(
453
+ "orf_detection",
454
+ "Find ORFs with explicit strand, start/stop, length, and translation-table assumptions.",
455
+ stage="analysis",
456
+ tokens=["orf", "open", "reading", "frame", "translate", "translation", "pyrodigal", "orfhunter"],
457
+ inputs=["text", "sequence_feature"],
458
+ outputs=["sequence_feature", "report_table"],
459
+ )
460
+ )
461
+ if "restriction_digest" in task_tags:
462
+ steps.append(
463
+ self._step_spec(
464
+ "restriction_digest",
465
+ "Resolve restriction enzyme recognition sites and compute digest fragments.",
466
+ stage="analysis",
467
+ tokens=["restriction", "digest", "enzyme", "cut", "fragment", "fragments", "site"],
468
+ inputs=["text", "sequence_feature"],
469
+ outputs=["sequence_feature", "report_table"],
470
+ )
471
+ )
472
+ if "primer_validation" in task_tags:
473
+ steps.append(
474
+ self._step_spec(
475
+ "primer_validation",
476
+ "Validate primer binding, orientation, Tm/GC, and expected amplicon or Gibson overlap.",
477
+ stage="analysis",
478
+ tokens=["primer", "primer3", "pcr", "gibson", "overlap", "amplicon", "tm", "decipher"],
479
+ inputs=["text", "sequence_feature"],
480
+ outputs=["sequence_feature", "report_table"],
481
+ )
482
+ )
483
+ if "sequence_alignment" in task_tags:
484
+ steps.append(
485
+ self._step_spec(
486
+ "sequence_alignment",
487
+ "Align primer, gene, or vector subsequences to validate exact or local matches.",
488
+ stage="analysis",
489
+ tokens=["align", "alignment", "mummer", "parasail", "match", "exact", "local"],
490
+ inputs=["text", "sequence_feature"],
491
+ outputs=["sequence_feature"],
492
+ )
493
+ )
494
+ if "kozak_scoring" in task_tags:
495
+ steps.append(
496
+ self._step_spec(
497
+ "kozak_scoring",
498
+ "Score Kozak context and translation-efficiency signals around candidate start codons.",
499
+ stage="analysis",
500
+ tokens=["kozak", "translation", "efficiency", "start", "codon", "rna"],
501
+ inputs=["text", "sequence_feature"],
502
+ outputs=["report_table"],
503
+ )
504
+ )
505
+ if "option_comparison" in task_tags:
506
+ steps.append(
507
+ self._step_spec(
508
+ "option_comparison",
509
+ "Map the computed result to the multiple-choice options and verify the final letter.",
510
+ stage="downstream",
511
+ tokens=["compare", "choice", "choices", "option", "answer", "verify"],
512
+ inputs=["report_table", "sequence_feature", "text"],
513
+ outputs=["report_table"],
514
+ )
515
+ )
516
+
517
+ if profile.get("has_sequence") and not profile.get("is_seqqa"):
518
+ steps.append(
519
+ self._step_spec(
520
+ "protein_identification",
521
+ "Ground the provided sequence to a protein or reference entity before downstream analysis.",
522
+ stage="input_acquisition",
523
+ tokens=["protein", "sequence", "reference", "identify", "uniprot", "blast", "ensembl", "homology"],
524
+ inputs=["text"],
525
+ outputs=["database_record"],
526
+ )
527
+ )
528
+
529
+ if profile.get("has_variant") and (profile.get("has_sequence") or profile.get("is_multiple_choice")):
530
+ steps.append(
531
+ self._step_spec(
532
+ "candidate_variant_extraction",
533
+ "Derive or compare the candidate variants against the grounded sequence context.",
534
+ stage="analysis",
535
+ tokens=["variant", "compare", "alignment", "mutation", "substitution", "sequence"],
536
+ inputs=["text", "database_record"],
537
+ outputs=["vcf", "annotated_variant_table"],
538
+ )
539
+ )
540
+
541
+ for resource in profile.get("resource_tokens", [])[:2]:
542
+ steps.append(
543
+ self._step_spec(
544
+ f"{resource}_lookup",
545
+ f"Query {resource} for task-specific evidence tied to the grounded entity or candidate variants.",
546
+ stage="analysis",
547
+ tokens=[resource, "query", "lookup", "record", "annotation", "database"],
548
+ inputs=["text", "database_record", "vcf", "annotated_variant_table"],
549
+ outputs=["database_record", "annotated_variant_table"],
550
+ )
551
+ )
552
+
553
+ if profile.get("needs_comparison"):
554
+ steps.append(
555
+ self._step_spec(
556
+ "option_comparison",
557
+ "Compare the competing options using the collected evidence and rank the supported candidate.",
558
+ stage="downstream",
559
+ tokens=["compare", "rank", "difference", "summary", "evidence", "supported"],
560
+ inputs=["database_record", "annotated_variant_table", "variant_summary"],
561
+ outputs=["variant_summary", "report_table"],
562
+ )
563
+ )
564
+
565
+ if not steps:
566
+ subtasks = [str(step).strip() for step in query_context.get("subtasks", []) if str(step).strip()]
567
+ for index, step in enumerate(subtasks[:4], start=1):
568
+ steps.append(
569
+ self._step_spec(
570
+ f"subtask_{index}",
571
+ step,
572
+ stage="analysis",
573
+ tokens=sorted(self._tokenize(step)),
574
+ inputs=task_spec.get("input_types", ["text"]),
575
+ outputs=[(task_spec.get("target_output") or {}).get("semantic_type") or "report_table"],
576
+ )
577
+ )
578
+
579
+ if not steps and ranked_paths:
580
+ best_path = ranked_paths[0]
581
+ for operation in best_path.get("operations", []):
582
+ operation_spec = self.schema_extractor.OPERATION_SPECS.get(operation, {})
583
+ steps.append(
584
+ self._step_spec(
585
+ operation,
586
+ f"Execute graph operation {operation}.",
587
+ stage=operation_spec.get("stage", "analysis"),
588
+ tokens=sorted(operation_spec.get("patterns", [])),
589
+ inputs=operation_spec.get("accepts", ["text"]),
590
+ outputs=operation_spec.get("produces", ["report_table"]),
591
+ )
592
+ )
593
+
594
+ return self._dedupe_steps(steps)
595
+
596
+ def _step_spec(
597
+ self,
598
+ label: str,
599
+ objective: str,
600
+ *,
601
+ stage: str,
602
+ tokens: list[str],
603
+ inputs: list[str],
604
+ outputs: list[str],
605
+ ) -> dict:
606
+ return {
607
+ "label": label,
608
+ "objective": objective,
609
+ "stage": stage,
610
+ "tokens": sorted({token.lower() for token in tokens if token}),
611
+ "inputs": inputs,
612
+ "outputs": outputs,
613
+ }
614
+
615
+ def _dedupe_steps(self, steps: list[dict]) -> list[dict]:
616
+ deduped = []
617
+ seen = set()
618
+ for step in steps:
619
+ key = step["label"]
620
+ if key in seen:
621
+ continue
622
+ seen.add(key)
623
+ deduped.append(step)
624
+ return deduped
625
+
626
+ def _select_tool_chain(
627
+ self,
628
+ step_specs: list[dict],
629
+ profile: dict,
630
+ tool_graph: ToolGraph,
631
+ *,
632
+ top_k_tools: int,
633
+ graph_rag_candidates: list[dict] | None = None,
634
+ ) -> list[dict]:
635
+ tool_plan = []
636
+ previous_step = None
637
+ used_nodes = set()
638
+ candidate_nodes = {item.get("node_id") for item in graph_rag_candidates or [] if item.get("node_id")}
639
+ candidate_scores = {item.get("node_id"): float(item.get("score", 0.0)) for item in graph_rag_candidates or []}
640
+ for step_index, step_spec in enumerate(step_specs, start=1):
641
+ candidate = self._best_tool_for_step(
642
+ step_spec,
643
+ profile,
644
+ tool_graph,
645
+ previous_step=previous_step,
646
+ used_nodes=used_nodes,
647
+ candidate_nodes=candidate_nodes,
648
+ candidate_scores=candidate_scores,
649
+ )
650
+ if candidate is None:
651
+ tool_plan.append(
652
+ {
653
+ "step": step_index,
654
+ "operation": step_spec["label"],
655
+ "server": "agent",
656
+ "tool": "reason_over_evidence",
657
+ "stage": step_spec["stage"],
658
+ "input_types": step_spec["inputs"],
659
+ "output_types": step_spec["outputs"],
660
+ "candidate_tool_count": 0,
661
+ "binding_source": "agent",
662
+ "planning_rationale": "no graph tool with strong semantic alignment; keep this as an agent reasoning step",
663
+ }
664
+ )
665
+ previous_step = tool_plan[-1]
666
+ continue
667
+
668
+ used_nodes.add(candidate["node_id"])
669
+ tool_plan.append(
670
+ {
671
+ "step": step_index,
672
+ "operation": step_spec["label"],
673
+ "server": candidate["server"],
674
+ "tool": candidate["name"],
675
+ "stage": candidate.get("stage") or step_spec["stage"],
676
+ "input_types": candidate.get("consumes") or step_spec["inputs"],
677
+ "output_types": candidate.get("produces") or step_spec["outputs"],
678
+ "candidate_tool_count": candidate["candidate_count"],
679
+ "binding_source": "graph",
680
+ "node_id": candidate["node_id"],
681
+ "planning_score": round(candidate["score"], 3),
682
+ "planning_rationale": candidate["reason"],
683
+ }
684
+ )
685
+ previous_step = tool_plan[-1]
686
+
687
+ return tool_plan[:top_k_tools]
688
+
689
+ def _best_tool_for_step(
690
+ self,
691
+ step_spec: dict,
692
+ profile: dict,
693
+ tool_graph: ToolGraph,
694
+ *,
695
+ previous_step: dict | None,
696
+ used_nodes: set[str],
697
+ candidate_nodes: set[str] | None = None,
698
+ candidate_scores: dict[str, float] | None = None,
699
+ ) -> dict | None:
700
+ scored = []
701
+ query_focus = set(profile.get("focus_tokens", []))
702
+ query_resources = set(profile.get("resource_tokens", []))
703
+ for server_name, payload in tool_graph.server_index.items():
704
+ for tool in payload.get("tools", []):
705
+ if candidate_nodes and tool.get("node_id") not in candidate_nodes:
706
+ continue
707
+ tool_text = " ".join(
708
+ [
709
+ server_name,
710
+ str(tool.get("name", "")),
711
+ str(tool.get("description", "")),
712
+ " ".join(tool.get("keywords", [])),
713
+ " ".join(tool.get("capabilities", [])),
714
+ " ".join(tool.get("consumes", [])),
715
+ " ".join(tool.get("produces", [])),
716
+ ]
717
+ ).lower()
718
+ tool_tokens = self._tokenize(tool_text)
719
+ score, reasons = self._score_tool_candidate(
720
+ tool,
721
+ server_name,
722
+ step_spec,
723
+ query_focus,
724
+ query_resources,
725
+ profile,
726
+ tool_graph,
727
+ previous_step,
728
+ reused=tool.get("node_id") in used_nodes,
729
+ )
730
+ if candidate_scores and tool.get("node_id") in candidate_scores:
731
+ score += min(candidate_scores[tool.get("node_id", "")], 12.0) * 0.35
732
+ reasons.append("graph_rag_prior")
733
+ if score <= 0.0:
734
+ continue
735
+ scored.append(
736
+ {
737
+ "score": score,
738
+ "reason": "; ".join(reasons[:4]) if reasons else "semantic alignment",
739
+ "reason_labels": reasons,
740
+ "node_id": tool["node_id"],
741
+ "server": server_name,
742
+ "name": tool.get("name"),
743
+ "stage": tool.get("stage"),
744
+ "consumes": tool.get("consumes", []),
745
+ "produces": tool.get("produces", []),
746
+ "tool_tokens": sorted(tool_tokens),
747
+ }
748
+ )
749
+
750
+ scored.sort(key=lambda item: (item["score"], item["server"], item["name"] or ""), reverse=True)
751
+ if not scored:
752
+ if candidate_nodes:
753
+ return self._best_tool_for_step(
754
+ step_spec,
755
+ profile,
756
+ tool_graph,
757
+ previous_step=previous_step,
758
+ used_nodes=used_nodes,
759
+ candidate_nodes=None,
760
+ candidate_scores=None,
761
+ )
762
+ return None
763
+ best = dict(scored[0])
764
+ best["candidate_count"] = len(scored)
765
+ if best["score"] < 4.5:
766
+ return None
767
+ if not self._candidate_meets_step_contract(best, step_spec, profile):
768
+ return None
769
+ return best
770
+
771
+ def _infer_task_tags(self, text: str, categories: set[str]) -> set[str]:
772
+ tags = set()
773
+ if "seqqa" in categories or "seqqa" in text or re.search(r"\b[acgtu]{30,}\b", text, flags=re.IGNORECASE):
774
+ tags.add("seqqa")
775
+ if self._asks_gc_content(text):
776
+ tags.update({"base_counting", "gc_content_calculation"})
777
+ if any(token in text for token in ("reverse complement", "revcomp")):
778
+ tags.add("reverse_complement")
779
+ if any(token in text for token in ("orf", "open reading frame", "aa encoded", "amino acid", "translation efficiency")):
780
+ tags.add("orf_detection")
781
+ if any(token in text for token in ("restriction", "digest", "enzyme", "fragment", "fragments")):
782
+ tags.add("restriction_digest")
783
+ if any(token in text for token in ("primer", "pcr", "gibson", "linearize", "linearized", "clone")):
784
+ tags.update({"primer_validation", "sequence_alignment"})
785
+ if any(token in text for token in ("kozak", "translation efficiency", "human cell")):
786
+ tags.add("kozak_scoring")
787
+ if any(token in text for token in ("which of the following", "choices", "(a)", "(b)")):
788
+ tags.add("option_comparison")
789
+ return tags
790
+
791
+ def _asks_gc_content(self, text: str) -> bool:
792
+ return bool(
793
+ re.search(r"\b(percent|percentage)\s+gc\b", text)
794
+ or re.search(r"\bgc\s+(content|percent|percentage)\b", text)
795
+ or re.search(r"\bwhat\s+is\s+the\s+gc\b", text)
796
+ )
797
+
798
+ def _score_tool_candidate(
799
+ self,
800
+ tool: dict,
801
+ server_name: str,
802
+ step_spec: dict,
803
+ query_focus: set[str],
804
+ query_resources: set[str],
805
+ profile: dict,
806
+ tool_graph: ToolGraph,
807
+ previous_step: dict | None,
808
+ *,
809
+ reused: bool,
810
+ ) -> tuple[float, list[str]]:
811
+ text = " ".join(
812
+ [
813
+ server_name,
814
+ str(tool.get("name", "")),
815
+ str(tool.get("description", "")),
816
+ " ".join(tool.get("keywords", [])),
817
+ " ".join(tool.get("capabilities", [])),
818
+ " ".join(tool.get("consumes", [])),
819
+ " ".join(tool.get("produces", [])),
820
+ ]
821
+ ).lower()
822
+ tool_tokens = self._tokenize(text)
823
+ step_tokens = set(step_spec.get("tokens", []))
824
+
825
+ score = 0.0
826
+ reasons = []
827
+
828
+ token_hits = step_tokens & tool_tokens
829
+ if token_hits:
830
+ score += min(len(token_hits), 6) * 1.4
831
+ reasons.append("step_tokens=" + ",".join(sorted(list(token_hits))[:4]))
832
+
833
+ focus_hits = query_focus & tool_tokens
834
+ if focus_hits:
835
+ score += min(len(focus_hits), 5) * 0.9
836
+ reasons.append("query_focus=" + ",".join(sorted(list(focus_hits))[:4]))
837
+
838
+ resource_hits = query_resources & tool_tokens
839
+ if resource_hits:
840
+ score += len(resource_hits) * 2.2
841
+ reasons.append("resource=" + ",".join(sorted(resource_hits)))
842
+
843
+ stage = tool.get("stage") or "analysis"
844
+ if stage == step_spec.get("stage"):
845
+ score += 1.2
846
+ reasons.append("stage_match")
847
+
848
+ desired_inputs = set(step_spec.get("inputs", []))
849
+ desired_outputs = set(step_spec.get("outputs", []))
850
+ tool_inputs = set(tool.get("consumes", []))
851
+ tool_outputs = set(tool.get("produces", []))
852
+ io_hits = (desired_inputs & tool_inputs) | (desired_outputs & tool_outputs)
853
+ if io_hits:
854
+ score += min(len(io_hits), 3) * 1.1
855
+ reasons.append("io_match=" + ",".join(sorted(list(io_hits))[:3]))
856
+ elif desired_outputs and tool_outputs and not (desired_outputs & tool_outputs):
857
+ score -= 1.8
858
+ reasons.append("io_mismatch")
859
+
860
+ step_label = step_spec.get("label", "")
861
+ if step_label == "protein_identification" and any(token in text for token in ("blast", "uniprot", "ensembl", "hhblits")):
862
+ score += 3.0
863
+ reasons.append("protein_grounding_tool")
864
+ if step_label == "candidate_variant_extraction" and any(
865
+ token in text for token in ("variant", "align", "blast", "consensus", "compare", "mutation")
866
+ ):
867
+ score += 2.5
868
+ reasons.append("variant_extraction_tool")
869
+ if step_label.endswith("_lookup") and any(token in text for token in ("query", "lookup", "search", "fetch", "database")):
870
+ score += 2.5
871
+ reasons.append("lookup_tool")
872
+ if step_label == "option_comparison" and any(token in text for token in ("compare", "summary", "annotation", "effect", "query")):
873
+ score += 1.8
874
+ reasons.append("comparison_support")
875
+ if step_label in {"base_counting", "gc_content_calculation"}:
876
+ if {"bases", "count", "atgc", "alphabet", "composition"} & tool_tokens:
877
+ score += 4.5
878
+ reasons.append("base_counting_tool")
879
+ if {"derep", "dereplicate", "duplicate", "grep", "filter", "extract"} & tool_tokens and not {
880
+ "bases",
881
+ "atgc",
882
+ "alphabet",
883
+ } & tool_tokens:
884
+ score -= 5.0
885
+ reasons.append("base_counting_wrong_operation_penalty")
886
+ if {"grep", "filter", "extract"} & tool_tokens and not {"bases", "atgc"} & tool_tokens:
887
+ score -= 3.5
888
+ reasons.append("base_counting_filter_penalty")
889
+ if step_label == "reverse_complement" and {"reverse", "complement", "rc"} & tool_tokens:
890
+ score += 4.0
891
+ reasons.append("reverse_complement_tool")
892
+ if step_label == "orf_detection" and {"orf", "orfs", "pyrodigal", "orfhunter", "translate"} & tool_tokens:
893
+ score += 5.0
894
+ reasons.append("orf_tool")
895
+ if step_label == "restriction_digest" and {"restriction", "digest", "enzyme", "cut", "fragment"} & tool_tokens:
896
+ score += 4.0
897
+ reasons.append("restriction_digest_tool")
898
+ if step_label == "primer_validation" and {"primer", "primer3", "pcr", "gibson", "tm"} & tool_tokens:
899
+ score += 5.0
900
+ reasons.append("primer_tool")
901
+ if step_label == "sequence_alignment" and {"align", "alignment", "mummer", "parasail", "match"} & tool_tokens:
902
+ score += 4.5
903
+ reasons.append("sequence_alignment_tool")
904
+ if step_label == "kozak_scoring" and {"kozak", "translation", "efficiency", "rna"} & tool_tokens:
905
+ score += 3.5
906
+ reasons.append("kozak_tool")
907
+ if step_label.endswith("_lookup") and query_resources and not resource_hits:
908
+ score -= 3.0
909
+ reasons.append("missing_named_resource")
910
+
911
+ if previous_step:
912
+ connectivity = self._tool_connectivity_score(tool, previous_step, tool_graph)
913
+ if connectivity > 0:
914
+ score += connectivity
915
+ reasons.append("graph_connected")
916
+
917
+ if reused:
918
+ score -= 0.4
919
+
920
+ if "orf" in tool_tokens and "orf" not in step_tokens and "coding" not in step_tokens:
921
+ score -= 4.5
922
+ reasons.append("orf_penalty")
923
+ if "promoter" in tool_tokens and "promoter" not in query_focus:
924
+ score -= 4.5
925
+ reasons.append("promoter_penalty")
926
+ if {"csv", "export"} & tool_tokens and step_label not in {"csv_export"}:
927
+ score -= 3.5
928
+ reasons.append("export_penalty")
929
+ if not self._is_sequencing_workflow(profile) and tool_inputs & self.SEQUENCING_INPUT_TYPES:
930
+ score -= 4.0
931
+ reasons.append("sequencing_workflow_penalty")
932
+ if self._wrong_domain_penalty(tool_tokens, profile):
933
+ score -= 3.5
934
+ reasons.append("wrong_domain_penalty")
935
+ if "spatial" in tool_tokens and "spatial" not in query_focus:
936
+ score -= 3.0
937
+ reasons.append("domain_penalty")
938
+ if profile.get("asks_clinical_significance") and "consensus" in tool_tokens:
939
+ score -= 1.5
940
+ reasons.append("consensus_penalty")
941
+ if profile.get("asks_clinical_significance") and step_label in {
942
+ "candidate_variant_extraction",
943
+ "option_comparison",
944
+ } and not ({"variant", "annotation", "effect", "mutation"} & tool_tokens):
945
+ score -= 2.5
946
+ reasons.append("clinical_variant_penalty")
947
+
948
+ return score, reasons
949
+
950
+ def _tool_connectivity_score(self, tool: dict, previous_step: dict, tool_graph: ToolGraph) -> float:
951
+ prev_node_id = previous_step.get("node_id")
952
+ if not prev_node_id or prev_node_id not in tool_graph.nodes:
953
+ return 0.0
954
+
955
+ current_node_id = tool.get("node_id")
956
+ if current_node_id == prev_node_id:
957
+ return 0.5
958
+
959
+ for edge in tool_graph.adjacency.get(prev_node_id, []):
960
+ if edge.get("target") == current_node_id and edge.get("type") in self.EDGE_TYPES:
961
+ return 2.5
962
+
963
+ prev_outputs = set(previous_step.get("output_types", []))
964
+ current_inputs = set(tool.get("consumes", []))
965
+ if prev_outputs & current_inputs:
966
+ return 1.8
967
+
968
+ return 0.0
969
+
970
+ def _build_type_trace(self, tool_plan: list[dict], task_spec: dict) -> list[str]:
971
+ trace = []
972
+ default_inputs = task_spec.get("input_types", []) or ["text"]
973
+ if default_inputs:
974
+ trace.append(default_inputs[0])
975
+ for step in tool_plan:
976
+ outputs = step.get("output_types", [])
977
+ if outputs:
978
+ trace.append(outputs[0])
979
+ elif trace:
980
+ trace.append(trace[-1])
981
+ return trace or ["text"]
982
+
983
+ def _tokenize(self, text: str) -> set[str]:
984
+ normalized = text.lower().replace("-", " ").replace("/", " ").replace("_", " ")
985
+ return {token for token in self.TOKEN_PATTERN.findall(normalized) if token}
986
+
987
+ def _is_sequencing_workflow(self, profile: dict) -> bool:
988
+ text = profile.get("text", "")
989
+ return any(
990
+ token in text
991
+ for token in ("fastq", "rna-seq", "rnaseq", "reads", "alignment", "count matrix", "raw counts")
992
+ )
993
+
994
+ def _wrong_domain_penalty(self, tool_tokens: set[str], profile: dict) -> bool:
995
+ text = profile.get("text", "")
996
+ allowed = {
997
+ token
998
+ for token in self.WRONG_DOMAIN_TOKENS
999
+ if token in text
1000
+ }
1001
+ return bool((tool_tokens & self.WRONG_DOMAIN_TOKENS) - allowed)
1002
+
1003
+ def _candidate_meets_step_contract(self, candidate: dict, step_spec: dict, profile: dict) -> bool:
1004
+ step_label = step_spec.get("label", "")
1005
+ tool_tokens = set(candidate.get("tool_tokens", []))
1006
+ tool_inputs = set(candidate.get("consumes", []))
1007
+ tool_outputs = set(candidate.get("produces", []))
1008
+ reasons = set(candidate.get("reason_labels", []))
1009
+
1010
+ if step_label == "protein_identification":
1011
+ return bool(
1012
+ {"protein_grounding_tool"} & reasons
1013
+ or {"blast", "uniprot", "ensembl", "hhblits", "homology"} & tool_tokens
1014
+ or "database_record" in tool_outputs
1015
+ )
1016
+
1017
+ if step_label == "candidate_variant_extraction":
1018
+ if not tool_outputs & {"vcf", "annotated_variant_table", "variant_summary"}:
1019
+ return False
1020
+ if not {"variant", "mutation", "annotation", "effect", "align", "compare"} & tool_tokens:
1021
+ return False
1022
+ if not self._is_sequencing_workflow(profile) and tool_inputs & self.SEQUENCING_INPUT_TYPES:
1023
+ return False
1024
+ return True
1025
+
1026
+ if step_label.endswith("_lookup"):
1027
+ resource_name = step_label.rsplit("_", 1)[0]
1028
+ has_named_resource = resource_name in tool_tokens
1029
+ has_lookup_shape = bool({"lookup_tool"} & reasons) and bool(
1030
+ tool_outputs & {"database_record", "annotated_variant_table", "variant_summary"}
1031
+ )
1032
+ return has_named_resource or has_lookup_shape
1033
+
1034
+ if step_label == "option_comparison":
1035
+ if not tool_outputs & {"variant_summary", "report_table", "database_record", "annotated_variant_table"}:
1036
+ return False
1037
+ if not {"compare", "summary", "effect", "annotation", "rank"} & tool_tokens:
1038
+ return False
1039
+ if tool_inputs & self.SEQUENCING_INPUT_TYPES:
1040
+ return False
1041
+ return True
1042
+
1043
+ if step_label in {"base_counting", "gc_content_calculation"}:
1044
+ return bool({"bases", "atgc", "alphabet"} & tool_tokens or "base_counting_tool" in reasons)
1045
+
1046
+ return True
Biomanus_upload/biomni/graph/schema_extractor.py ADDED
@@ -0,0 +1,1180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections import Counter
5
+
6
+ from biomni.graph.workflow_priors import WORKFLOW_PRIORS, rank_workflow_priors
7
+
8
+
9
+ class ToolSchemaExtractor:
10
+ """Infer lightweight graph semantics from MCP/tool metadata."""
11
+
12
+ CAPABILITY_PATTERNS = {
13
+ "normalization": {"normalize", "normalization", "normalise", "normalisation", "size factor", "vst", "rlog"},
14
+ "quantification": {"quant", "quantification", "salmon", "kallisto", "abundance", "transcript"},
15
+ "read_counting": {"featurecounts", "htseq", "count reads", "read count", "summarizeoverlaps"},
16
+ "differential_expression": {
17
+ "deseq2",
18
+ "differential expression",
19
+ "differentially expressed",
20
+ "deg",
21
+ "edger",
22
+ "limma",
23
+ "log2fc",
24
+ "fold change",
25
+ },
26
+ "pathway_enrichment": {
27
+ "gsea",
28
+ "fgsea",
29
+ "clusterprofiler",
30
+ "enrichment",
31
+ "gene set",
32
+ "msigdb",
33
+ "kegg",
34
+ "reactome",
35
+ "gene ontology",
36
+ },
37
+ "data_inspection": {"inspect", "summary", "schema", "preview", "describe", "table"},
38
+ "exact_sequence_analysis": {"exact short-sequence primitive", "exact sequence primitive"},
39
+ "format_conversion": {"convert", "transform", "parse", "merge", "filter", "export"},
40
+ "single_cell_analysis": {"single-cell", "singlecell", "scrna", "scanpy", "seurat", "clustering", "umap"},
41
+ "alignment": {"align", "alignment", "bwa", "star", "map"},
42
+ "variant_calling": {"variant", "vcf", "gatk", "snp", "mutation"},
43
+ "variant_filtering": {"filter variant", "bcftools filter", "vcf filter", "normalize variant"},
44
+ "taxonomic_classification": {"taxonomy", "taxonomic", "kraken", "metaphlan", "centrifuge", "bracken"},
45
+ "genome_annotation": {"prokka", "annotate genome", "genome annotation"},
46
+ "orthology_clustering": {"orthofinder", "ortholog", "orthogroup", "cluster annotation"},
47
+ "annotation": {"annotate", "annotation", "label", "classify"},
48
+ "visualization": {"plot", "visual", "umap", "tsne", "heatmap", "volcano"},
49
+ "database_search": {
50
+ "database",
51
+ "lookup",
52
+ "search",
53
+ "pubmed",
54
+ "ensembl",
55
+ "clinvar",
56
+ "uniprot",
57
+ "biomart",
58
+ "msigdb",
59
+ "mirdb",
60
+ "gtrd",
61
+ "gene set",
62
+ },
63
+ "quality_control": {"qc", "quality", "filter", "trim", "clean"},
64
+ "statistics": {"stat", "qvalue", "pvalue", "regression", "test", "significance"},
65
+ }
66
+
67
+ DB_RESOURCE_HINTS = {
68
+ "ensembl",
69
+ "clinvar",
70
+ "uniprot",
71
+ "pubmed",
72
+ "biomart",
73
+ "msigdb",
74
+ "mirdb",
75
+ "gtrd",
76
+ "gene set",
77
+ "ontology",
78
+ "disease ontology",
79
+ "cell atlas",
80
+ "annotation database",
81
+ "metadata query",
82
+ "association table",
83
+ }
84
+
85
+ PATHWAY_HINTS = {
86
+ "gsea",
87
+ "fgsea",
88
+ "clusterprofiler",
89
+ "enrichment",
90
+ "gene set",
91
+ "pathway",
92
+ "kegg",
93
+ "reactome",
94
+ "gene ontology",
95
+ "msigdb",
96
+ }
97
+
98
+ SINGLE_CELL_HINTS = {
99
+ "single-cell",
100
+ "singlecell",
101
+ "scrna",
102
+ "scanpy",
103
+ "seurat",
104
+ "singlecellexperiment",
105
+ "cell atlas",
106
+ "cell type",
107
+ "umap",
108
+ }
109
+
110
+ DIFFERENTIAL_EXPRESSION_HINTS = {
111
+ "differential expression",
112
+ "deseq2",
113
+ "edger",
114
+ "limma",
115
+ "deg",
116
+ "fold change",
117
+ "log2fc",
118
+ "padj",
119
+ }
120
+
121
+ SERVER_CAPABILITY_ALLOWLISTS = {
122
+ "bwa": {"alignment"},
123
+ "hisat2": {"alignment"},
124
+ "star": {"alignment"},
125
+ "kallisto": {"quantification"},
126
+ "salmon": {"quantification"},
127
+ "bioconductor-tximport": {"quantification", "format_conversion", "read_counting"},
128
+ "bioconductor-deseq2": {"normalization", "differential_expression", "statistics", "visualization"},
129
+ "cnvkit": {"variant_calling", "variant_filtering", "annotation", "visualization"},
130
+ "samtools": {"alignment", "format_conversion", "data_inspection"},
131
+ "bcftools": {"variant_calling", "variant_filtering", "annotation", "format_conversion"},
132
+ "gatk": {"variant_calling", "variant_filtering"},
133
+ "gatk4": {"variant_calling", "variant_filtering"},
134
+ "freebayes": {"variant_calling"},
135
+ "ucsc-cell-browser": {"single_cell_analysis", "visualization", "data_inspection"},
136
+ "vpt": {"statistics", "annotation", "format_conversion"},
137
+ "r-dwls": {"statistics", "single_cell_analysis"},
138
+ "seacells": {"single_cell_analysis"},
139
+ "harpy": {"single_cell_analysis", "statistics"},
140
+ }
141
+
142
+ DATATYPE_PATTERNS = {
143
+ "anndata": {"anndata", "h5ad"},
144
+ "annotated_variant_table": {"annotated variant", "variant annotation table", "annotated vcf"},
145
+ "fastq": {"fastq", "fasta", "reads"},
146
+ "metagenomic_reads": {"metagenomic reads", "metagenomics reads", "microbiome reads"},
147
+ "bam": {"bam", "sam"},
148
+ "vcf": {"vcf", "variant"},
149
+ "reference_genome": {"reference genome", "genome index", "fasta", "genomic fna"},
150
+ "annotation_gff": {"gff", "gtf", "genomic annotation"},
151
+ "quant_table": {"quant.sf", "abundance table", "transcript abundance"},
152
+ "csv": {"csv", "tsv", "table"},
153
+ "json": {"json"},
154
+ "rds": {"rds", "seurat", "spatial object"},
155
+ "count_matrix": {"count", "counts", "matrix", "raw counts"},
156
+ "raw_count_matrix": {
157
+ "raw count",
158
+ "raw count matrix",
159
+ "raw count matrices",
160
+ "raw counts",
161
+ "integer counts",
162
+ "count matrix",
163
+ "count matrices",
164
+ },
165
+ "normalized_count_matrix": {"normalized count", "normalised count", "vst", "rlog", "size factor"},
166
+ "sample_metadata": {"sample metadata", "metadata table", "condition", "phenotype", "group labels"},
167
+ "de_table": {"differential expression", "deg", "log2fc", "padj", "de table"},
168
+ "differential_expression_table": {
169
+ "differential expression table",
170
+ "differential expression",
171
+ "deg",
172
+ "log2fc",
173
+ "padj",
174
+ "de table",
175
+ },
176
+ "significant_gene_list": {"significant genes", "up-regulated genes", "down-regulated genes", "de genes"},
177
+ "ranked_gene_list": {"ranked list", "ranked genes", "prerank", "gene ranking"},
178
+ "gene_list": {"gene list", "genes"},
179
+ "gene_symbol_list": {"gene symbol", "gene symbols", "symbol list"},
180
+ "entrez_gene_list": {"entrez", "entrez id", "entrez gene"},
181
+ "pathway_table": {"pathway", "enrichment", "gsea result"},
182
+ "kegg_enrichment_table": {"kegg enrichment", "enrichkegg", "kegg pathway"},
183
+ "go_enrichment_table": {"go enrichment", "gene ontology enrichment"},
184
+ "merged_pathway_table": {"merged pathway", "shared pathway", "comparative pathway"},
185
+ "taxonomic_profile": {"taxonomic profile", "relative abundance", "taxonomy table"},
186
+ "differential_abundance_table": {"differential abundance", "abundance difference"},
187
+ "genome_assembly": {"assembly", "genome assembly", "contig", "scaffold"},
188
+ "cluster_table": {"cluster table", "cluster annotation", "orthogroup"},
189
+ "marker_gene_table": {"marker gene", "cluster marker", "cell type marker"},
190
+ "database_record": {"database record", "knowledge source", "annotation record", "metadata entry"},
191
+ "sequence_feature": {"motif", "primer", "binding site", "sequence feature"},
192
+ "variant_summary": {"variant summary", "variant report"},
193
+ "report_table": {"final table", "output table", "result table"},
194
+ "image": {"png", "pdf", "plot", "figure", "image"},
195
+ "text": {"txt", "text", "markdown", "report", "summary"},
196
+ }
197
+
198
+ OPERATION_SPECS = {
199
+ "database_lookup": {
200
+ "patterns": {"database", "lookup", "search", "disgenet", "omim", "clinvar", "ensembl", "uniprot"},
201
+ "accepts": ["text", "gene_symbol_list"],
202
+ "produces": ["database_record"],
203
+ "capabilities": ["database_search", "annotation"],
204
+ "stage": "input_acquisition",
205
+ },
206
+ "sequence_feature_extraction": {
207
+ "patterns": {"sequence", "fasta", "motif", "primer", "binding site", "reverse complement"},
208
+ "accepts": ["text", "fastq", "reference_genome"],
209
+ "produces": ["sequence_feature"],
210
+ "capabilities": ["alignment", "annotation", "format_conversion"],
211
+ "stage": "analysis",
212
+ },
213
+ "base_counting": {
214
+ "patterns": {"base count", "count bases", "nucleotide count", "composition", "alphabet frequency"},
215
+ "accepts": ["text", "sequence_feature"],
216
+ "produces": ["sequence_feature", "report_table"],
217
+ "capabilities": ["statistics", "data_inspection", "exact_sequence_analysis"],
218
+ "stage": "analysis",
219
+ },
220
+ "gc_content_calculation": {
221
+ "patterns": {"gc content", "percent gc", "gc percentage", "base composition"},
222
+ "accepts": ["text", "sequence_feature"],
223
+ "produces": ["report_table"],
224
+ "capabilities": ["statistics", "data_inspection", "exact_sequence_analysis"],
225
+ "stage": "analysis",
226
+ },
227
+ "reverse_complement": {
228
+ "patterns": {"reverse complement", "revcomp", "reverse-complement", "rc sequence"},
229
+ "accepts": ["text", "sequence_feature"],
230
+ "produces": ["sequence_feature"],
231
+ "capabilities": ["format_conversion", "sequence_feature_extraction", "exact_sequence_analysis"],
232
+ "stage": "analysis",
233
+ },
234
+ "orf_detection": {
235
+ "patterns": {"orf", "open reading frame", "translate", "translation", "coding sequence"},
236
+ "accepts": ["text", "sequence_feature"],
237
+ "produces": ["sequence_feature", "report_table"],
238
+ "capabilities": ["annotation", "sequence_feature_extraction", "exact_sequence_analysis"],
239
+ "stage": "analysis",
240
+ },
241
+ "restriction_digest": {
242
+ "patterns": {"restriction digest", "digest", "restriction enzyme", "cut site", "fragments"},
243
+ "accepts": ["text", "sequence_feature"],
244
+ "produces": ["sequence_feature", "report_table"],
245
+ "capabilities": ["sequence_feature_extraction", "data_inspection", "exact_sequence_analysis"],
246
+ "stage": "analysis",
247
+ },
248
+ "primer_validation": {
249
+ "patterns": {"primer", "pcr", "gibson", "amplicon", "primer pair", "tm", "linearize", "linearized"},
250
+ "accepts": ["text", "sequence_feature"],
251
+ "produces": ["sequence_feature", "report_table"],
252
+ "capabilities": ["sequence_feature_extraction", "alignment", "exact_sequence_analysis"],
253
+ "stage": "analysis",
254
+ },
255
+ "sequence_alignment": {
256
+ "patterns": {"align sequence", "sequence alignment", "local alignment", "exact match", "pairwise alignment"},
257
+ "accepts": ["text", "sequence_feature"],
258
+ "produces": ["sequence_feature"],
259
+ "capabilities": ["alignment"],
260
+ "stage": "analysis",
261
+ },
262
+ "kozak_scoring": {
263
+ "patterns": {"kozak", "translation efficiency", "start codon context", "human cell"},
264
+ "accepts": ["text", "sequence_feature"],
265
+ "produces": ["report_table"],
266
+ "capabilities": ["sequence_feature_extraction", "statistics", "exact_sequence_analysis"],
267
+ "stage": "analysis",
268
+ },
269
+ "quality_control": {
270
+ "patterns": {"fastqc", "multiqc", "quality control", "qc"},
271
+ "accepts": ["fastq"],
272
+ "produces": ["text", "image"],
273
+ "capabilities": ["quality_control"],
274
+ "stage": "preprocessing",
275
+ },
276
+ "quantification": {
277
+ "patterns": {"quantify", "quantification", "salmon", "kallisto", "abundance", "quant.sf"},
278
+ "accepts": ["fastq", "bam", "reference_genome"],
279
+ "produces": ["quant_table"],
280
+ "capabilities": ["quantification"],
281
+ "stage": "analysis",
282
+ },
283
+ "alignment": {
284
+ "patterns": {"align reads", "alignment", "hisat2", "star", "bwa", "bowtie2", "map reads"},
285
+ "accepts": ["fastq", "reference_genome"],
286
+ "produces": ["bam"],
287
+ "capabilities": ["alignment"],
288
+ "stage": "preprocessing",
289
+ },
290
+ "read_counting": {
291
+ "patterns": {"featurecounts", "htseq", "count reads", "read counts", "gene count"},
292
+ "accepts": ["bam", "annotation_gff"],
293
+ "produces": ["raw_count_matrix"],
294
+ "capabilities": ["read_counting"],
295
+ "stage": "analysis",
296
+ },
297
+ "count_normalization": {
298
+ "patterns": {"normalize counts", "normalise counts", "normalization", "size factor", "vst", "rlog"},
299
+ "accepts": ["raw_count_matrix", "sample_metadata"],
300
+ "produces": ["normalized_count_matrix"],
301
+ "capabilities": ["normalization"],
302
+ "stage": "analysis",
303
+ },
304
+ "differential_expression": {
305
+ "patterns": {"differential expression", "deseq2", "deg", "edger", "limma", "log2fc", "padj"},
306
+ "accepts": ["raw_count_matrix", "normalized_count_matrix", "sample_metadata"],
307
+ "produces": ["differential_expression_table"],
308
+ "capabilities": ["differential_expression", "statistics"],
309
+ "constraints": ["requires_replicates", "bulk_rna_seq"],
310
+ "stage": "analysis",
311
+ },
312
+ "gene_filtering": {
313
+ "patterns": {"filter genes", "significant genes", "up-regulated", "down-regulated", "padj"},
314
+ "accepts": ["differential_expression_table"],
315
+ "produces": ["significant_gene_list", "gene_symbol_list"],
316
+ "capabilities": ["format_conversion", "statistics"],
317
+ "stage": "downstream",
318
+ },
319
+ "gene_id_conversion": {
320
+ "patterns": {"gene id", "convert gene", "entrez", "symbol", "ortholog", "id conversion"},
321
+ "accepts": ["gene_symbol_list", "significant_gene_list"],
322
+ "produces": ["entrez_gene_list"],
323
+ "capabilities": ["annotation", "format_conversion", "genome_annotation"],
324
+ "stage": "downstream",
325
+ },
326
+ "kegg_enrichment": {
327
+ "patterns": {"kegg", "pathway enrichment", "enrichkegg", "clusterprofiler"},
328
+ "accepts": ["entrez_gene_list", "ranked_gene_list", "significant_gene_list"],
329
+ "produces": ["kegg_enrichment_table"],
330
+ "capabilities": ["pathway_enrichment"],
331
+ "constraints": ["requires_entrez_id"],
332
+ "stage": "downstream",
333
+ },
334
+ "go_enrichment": {
335
+ "patterns": {"go enrichment", "gene ontology", "enrichgo", "ontology"},
336
+ "accepts": ["entrez_gene_list", "ranked_gene_list", "significant_gene_list"],
337
+ "produces": ["go_enrichment_table"],
338
+ "capabilities": ["pathway_enrichment"],
339
+ "stage": "downstream",
340
+ },
341
+ "pathway_merge": {
342
+ "patterns": {"compare shared pathways", "shared pathways", "merge pathways", "compare pathways"},
343
+ "accepts": ["kegg_enrichment_table", "go_enrichment_table", "pathway_table"],
344
+ "produces": ["merged_pathway_table"],
345
+ "capabilities": ["format_conversion", "data_inspection"],
346
+ "stage": "reporting",
347
+ },
348
+ "csv_export": {
349
+ "patterns": {"csv", "output table", "save table", "export"},
350
+ "accepts": [
351
+ "merged_pathway_table",
352
+ "kegg_enrichment_table",
353
+ "go_enrichment_table",
354
+ "pathway_table",
355
+ "differential_expression_table",
356
+ "variant_summary",
357
+ "taxonomic_profile",
358
+ ],
359
+ "produces": ["report_table"],
360
+ "capabilities": ["format_conversion"],
361
+ "stage": "reporting",
362
+ },
363
+ "variant_annotation": {
364
+ "patterns": {"variant annotation", "vep", "snpeff", "annotate variant", "clinvar"},
365
+ "accepts": ["vcf"],
366
+ "produces": ["annotated_variant_table"],
367
+ "capabilities": ["annotation", "variant_filtering"],
368
+ "stage": "analysis",
369
+ },
370
+ "variant_summary": {
371
+ "patterns": {"variant summary", "summarize variants", "variant report"},
372
+ "accepts": ["annotated_variant_table", "vcf"],
373
+ "produces": ["variant_summary"],
374
+ "capabilities": ["data_inspection", "format_conversion"],
375
+ "stage": "reporting",
376
+ },
377
+ "single_cell_clustering": {
378
+ "patterns": {"single-cell", "singlecell", "scrna", "scanpy", "seurat", "clustering", "umap"},
379
+ "accepts": ["anndata", "rds"],
380
+ "produces": ["cluster_table"],
381
+ "capabilities": ["single_cell_analysis"],
382
+ "stage": "analysis",
383
+ },
384
+ "marker_gene_detection": {
385
+ "patterns": {"marker genes", "cluster markers", "cell type markers"},
386
+ "accepts": ["anndata", "cluster_table"],
387
+ "produces": ["marker_gene_table", "gene_symbol_list"],
388
+ "capabilities": ["single_cell_analysis", "statistics"],
389
+ "stage": "downstream",
390
+ },
391
+ "taxonomic_classification": {
392
+ "patterns": {"taxonomy", "taxonomic", "kraken", "metaphlan", "centrifuge", "bracken"},
393
+ "accepts": ["fastq", "metagenomic_reads"],
394
+ "produces": ["taxonomic_profile"],
395
+ "capabilities": ["taxonomic_classification"],
396
+ "stage": "analysis",
397
+ },
398
+ "differential_abundance": {
399
+ "patterns": {"differential abundance", "abundance difference", "compare abundance"},
400
+ "accepts": ["taxonomic_profile", "sample_metadata"],
401
+ "produces": ["differential_abundance_table"],
402
+ "capabilities": ["statistics"],
403
+ "stage": "downstream",
404
+ },
405
+ }
406
+
407
+ STAGE_RULES = [
408
+ ("input_acquisition", {"database_search"}),
409
+ (
410
+ "preprocessing",
411
+ {"data_inspection", "format_conversion", "quality_control", "alignment", "normalization"},
412
+ ),
413
+ (
414
+ "analysis",
415
+ {
416
+ "quantification",
417
+ "read_counting",
418
+ "differential_expression",
419
+ "single_cell_analysis",
420
+ "variant_calling",
421
+ "variant_filtering",
422
+ "taxonomic_classification",
423
+ "orthology_clustering",
424
+ "statistics",
425
+ },
426
+ ),
427
+ ("downstream", {"pathway_enrichment", "annotation", "genome_annotation"}),
428
+ ("reporting", {"visualization"}),
429
+ ]
430
+
431
+ STOPWORDS = {
432
+ "the",
433
+ "and",
434
+ "for",
435
+ "with",
436
+ "from",
437
+ "that",
438
+ "this",
439
+ "into",
440
+ "using",
441
+ "data",
442
+ "tool",
443
+ "file",
444
+ "files",
445
+ "analysis",
446
+ "result",
447
+ "results",
448
+ "input",
449
+ "output",
450
+ }
451
+
452
+ CURATED_SERVER_SEMANTICS = {
453
+ "salmon": {
454
+ "capabilities": ["quantification"],
455
+ "consumes": ["fastq", "reference_genome"],
456
+ "produces": ["quant_table"],
457
+ "operations": ["quantification"],
458
+ "stage": "analysis",
459
+ },
460
+ "bioconductor-tximport": {
461
+ "capabilities": ["quantification", "format_conversion"],
462
+ "consumes": ["quant_table", "annotation_gff"],
463
+ "produces": ["count_matrix"],
464
+ "operations": ["read_counting"],
465
+ "stage": "analysis",
466
+ },
467
+ "bioconductor-deseq2": {
468
+ "capabilities": ["normalization", "differential_expression", "statistics", "visualization"],
469
+ "consumes": ["raw_count_matrix", "sample_metadata", "count_matrix"],
470
+ "produces": ["differential_expression_table", "de_table", "image"],
471
+ "operations": ["count_normalization", "differential_expression", "gene_filtering"],
472
+ "stage": "analysis",
473
+ },
474
+ "hisat2": {
475
+ "capabilities": ["alignment"],
476
+ "consumes": ["fastq", "reference_genome", "annotation_gff"],
477
+ "produces": ["bam"],
478
+ "operations": ["alignment"],
479
+ "stage": "preprocessing",
480
+ },
481
+ "star": {
482
+ "capabilities": ["alignment"],
483
+ "consumes": ["fastq", "reference_genome", "annotation_gff"],
484
+ "produces": ["bam"],
485
+ "operations": ["alignment"],
486
+ "stage": "preprocessing",
487
+ },
488
+ "htseq": {
489
+ "capabilities": ["read_counting"],
490
+ "consumes": ["bam", "annotation_gff"],
491
+ "produces": ["raw_count_matrix", "count_matrix"],
492
+ "operations": ["read_counting"],
493
+ "stage": "analysis",
494
+ },
495
+ "subread": {
496
+ "capabilities": ["alignment", "read_counting"],
497
+ "consumes": ["fastq", "bam", "reference_genome", "annotation_gff"],
498
+ "produces": ["bam", "raw_count_matrix", "count_matrix"],
499
+ "operations": ["alignment", "read_counting"],
500
+ "stage": "analysis",
501
+ },
502
+ "fastqc": {
503
+ "capabilities": ["quality_control"],
504
+ "consumes": ["fastq"],
505
+ "produces": ["text"],
506
+ "operations": ["quality_control"],
507
+ "stage": "preprocessing",
508
+ },
509
+ "multiqc": {
510
+ "capabilities": ["quality_control", "visualization"],
511
+ "consumes": ["text"],
512
+ "produces": ["text", "image"],
513
+ "operations": ["quality_control", "csv_export"],
514
+ "stage": "reporting",
515
+ },
516
+ "gffutils": {
517
+ "capabilities": ["genome_annotation", "format_conversion", "data_inspection"],
518
+ "consumes": ["annotation_gff", "reference_genome"],
519
+ "produces": ["annotation_gff", "gene_list", "gene_symbol_list", "csv"],
520
+ "operations": ["gene_id_conversion", "database_lookup"],
521
+ "stage": "preprocessing",
522
+ },
523
+ "bcftools": {
524
+ "capabilities": ["variant_calling", "variant_filtering", "annotation"],
525
+ "consumes": ["bam", "vcf", "reference_genome"],
526
+ "produces": ["vcf", "annotated_variant_table", "variant_summary"],
527
+ "operations": ["variant_annotation", "variant_summary"],
528
+ "stage": "analysis",
529
+ },
530
+ "seqfu": {
531
+ "capabilities": ["data_inspection", "format_conversion", "statistics"],
532
+ "consumes": ["text", "fastq", "sequence_feature"],
533
+ "produces": ["text", "sequence_feature", "report_table"],
534
+ "operations": ["base_counting", "gc_content_calculation", "reverse_complement", "sequence_feature_extraction"],
535
+ "stage": "analysis",
536
+ },
537
+ "bioawk": {
538
+ "capabilities": ["data_inspection", "format_conversion", "statistics"],
539
+ "consumes": ["text", "fastq", "sequence_feature"],
540
+ "produces": ["text", "sequence_feature", "report_table"],
541
+ "operations": ["base_counting", "gc_content_calculation", "sequence_feature_extraction"],
542
+ "stage": "analysis",
543
+ },
544
+ "primer3": {
545
+ "capabilities": ["sequence_feature_extraction", "alignment", "statistics"],
546
+ "consumes": ["text", "sequence_feature"],
547
+ "produces": ["sequence_feature", "report_table"],
548
+ "operations": ["primer_validation", "sequence_feature_extraction"],
549
+ "stage": "analysis",
550
+ },
551
+ "bioconductor-decipher": {
552
+ "capabilities": ["alignment", "annotation", "sequence_feature_extraction"],
553
+ "consumes": ["text", "sequence_feature", "fastq", "reference_genome"],
554
+ "produces": ["sequence_feature", "report_table"],
555
+ "operations": ["primer_validation", "sequence_alignment", "sequence_feature_extraction"],
556
+ "stage": "analysis",
557
+ },
558
+ "pyrodigal": {
559
+ "capabilities": ["annotation", "sequence_feature_extraction"],
560
+ "consumes": ["text", "sequence_feature", "reference_genome"],
561
+ "produces": ["sequence_feature", "report_table"],
562
+ "operations": ["orf_detection", "sequence_feature_extraction"],
563
+ "stage": "analysis",
564
+ },
565
+ "mummer": {
566
+ "capabilities": ["alignment", "data_inspection"],
567
+ "consumes": ["text", "sequence_feature", "reference_genome"],
568
+ "produces": ["sequence_feature", "report_table"],
569
+ "operations": ["sequence_alignment", "primer_validation"],
570
+ "stage": "analysis",
571
+ },
572
+ "parasail-python": {
573
+ "capabilities": ["alignment", "statistics"],
574
+ "consumes": ["text", "sequence_feature"],
575
+ "produces": ["sequence_feature", "report_table"],
576
+ "operations": ["sequence_alignment", "primer_validation"],
577
+ "stage": "analysis",
578
+ },
579
+ "viennarna": {
580
+ "capabilities": ["sequence_feature_extraction", "statistics"],
581
+ "consumes": ["text", "sequence_feature"],
582
+ "produces": ["sequence_feature", "report_table"],
583
+ "operations": ["kozak_scoring", "sequence_feature_extraction"],
584
+ "stage": "analysis",
585
+ },
586
+ "sequence-operations": {
587
+ "capabilities": ["sequence_feature_extraction", "statistics", "data_inspection", "alignment"],
588
+ "consumes": ["text", "sequence_feature"],
589
+ "produces": ["sequence_feature", "report_table"],
590
+ "operations": ["sequence_feature_extraction"],
591
+ "stage": "analysis",
592
+ },
593
+ "local-bio-cache": {
594
+ "capabilities": ["database_search", "alignment", "data_inspection"],
595
+ "consumes": ["text", "sequence_feature", "reference_genome"],
596
+ "produces": ["database_record", "sequence_feature", "report_table"],
597
+ "operations": ["database_lookup", "sequence_alignment", "sequence_feature_extraction"],
598
+ "stage": "analysis",
599
+ },
600
+ "gatk": {
601
+ "capabilities": ["variant_calling", "variant_filtering"],
602
+ "consumes": ["bam", "reference_genome"],
603
+ "produces": ["vcf"],
604
+ "operations": ["variant_annotation"],
605
+ "stage": "analysis",
606
+ },
607
+ "freebayes": {
608
+ "capabilities": ["variant_calling"],
609
+ "consumes": ["bam", "reference_genome"],
610
+ "produces": ["vcf"],
611
+ "operations": ["variant_annotation"],
612
+ "stage": "analysis",
613
+ },
614
+ "kraken2": {
615
+ "capabilities": ["taxonomic_classification"],
616
+ "consumes": ["fastq", "metagenomic_reads"],
617
+ "produces": ["taxonomic_profile"],
618
+ "operations": ["taxonomic_classification"],
619
+ "stage": "analysis",
620
+ },
621
+ "metaphlan": {
622
+ "capabilities": ["taxonomic_classification"],
623
+ "consumes": ["fastq", "metagenomic_reads"],
624
+ "produces": ["taxonomic_profile"],
625
+ "operations": ["taxonomic_classification"],
626
+ "stage": "analysis",
627
+ },
628
+ "prokka": {
629
+ "capabilities": ["genome_annotation"],
630
+ "consumes": ["genome_assembly"],
631
+ "produces": ["annotation_gff", "gene_list"],
632
+ "operations": ["gene_id_conversion"],
633
+ "stage": "analysis",
634
+ },
635
+ "orthofinder": {
636
+ "capabilities": ["orthology_clustering"],
637
+ "consumes": ["gene_list"],
638
+ "produces": ["cluster_table"],
639
+ "operations": ["gene_id_conversion"],
640
+ "stage": "analysis",
641
+ },
642
+ }
643
+
644
+ CURATED_TOOL_SEMANTICS = {
645
+ ("sequence-operations", "gc_content"): {
646
+ "capabilities": ["statistics", "data_inspection"],
647
+ "consumes": ["text", "sequence_feature"],
648
+ "produces": ["report_table"],
649
+ "operations": ["base_counting", "gc_content_calculation"],
650
+ "stage": "analysis",
651
+ "override": True,
652
+ },
653
+ ("sequence-operations", "reverse_complement"): {
654
+ "capabilities": ["format_conversion", "sequence_feature_extraction", "exact_sequence_analysis"],
655
+ "consumes": ["text", "sequence_feature"],
656
+ "produces": ["sequence_feature"],
657
+ "operations": ["reverse_complement"],
658
+ "stage": "analysis",
659
+ "override": True,
660
+ },
661
+ ("sequence-operations", "find_orfs"): {
662
+ "capabilities": ["annotation", "sequence_feature_extraction", "exact_sequence_analysis"],
663
+ "consumes": ["text", "sequence_feature"],
664
+ "produces": ["sequence_feature", "report_table"],
665
+ "operations": ["orf_detection"],
666
+ "stage": "analysis",
667
+ "override": True,
668
+ },
669
+ ("sequence-operations", "amino_acid_at_position_in_longest_orf"): {
670
+ "capabilities": ["annotation", "sequence_feature_extraction", "exact_sequence_analysis"],
671
+ "consumes": ["text", "sequence_feature"],
672
+ "produces": ["sequence_feature", "report_table"],
673
+ "operations": ["orf_detection"],
674
+ "stage": "analysis",
675
+ "override": True,
676
+ },
677
+ ("sequence-operations", "restriction_digest"): {
678
+ "capabilities": ["sequence_feature_extraction", "data_inspection", "exact_sequence_analysis"],
679
+ "consumes": ["text", "sequence_feature"],
680
+ "produces": ["sequence_feature", "report_table"],
681
+ "operations": ["restriction_digest"],
682
+ "stage": "analysis",
683
+ "override": True,
684
+ },
685
+ ("sequence-operations", "pcr_primer_analysis"): {
686
+ "capabilities": ["sequence_feature_extraction", "alignment", "exact_sequence_analysis"],
687
+ "consumes": ["text", "sequence_feature"],
688
+ "produces": ["sequence_feature", "report_table"],
689
+ "operations": ["primer_validation", "sequence_alignment"],
690
+ "stage": "analysis",
691
+ "override": True,
692
+ },
693
+ ("sequence-operations", "kozak_score"): {
694
+ "capabilities": ["sequence_feature_extraction", "statistics", "exact_sequence_analysis"],
695
+ "consumes": ["text", "sequence_feature"],
696
+ "produces": ["report_table"],
697
+ "operations": ["kozak_scoring"],
698
+ "stage": "analysis",
699
+ "override": True,
700
+ },
701
+ }
702
+
703
+ def tokenize(self, text: str) -> set[str]:
704
+ return {
705
+ token
706
+ for token in re.findall(r"[a-z0-9_\-\.]{2,}", (text or "").lower())
707
+ if token not in self.STOPWORDS
708
+ }
709
+
710
+ def extract_tool_semantics(self, tool: dict, server_name: str | None = None) -> dict:
711
+ name = str(tool.get("name", ""))
712
+ description = str(tool.get("description", ""))
713
+ params = tool.get("inputSchema", {}).get("properties", {}) or tool.get("parameters", {}) or {}
714
+ param_text = " ".join(
715
+ f"{param_name} {spec.get('type', '')} {spec.get('description', '')}" for param_name, spec in params.items()
716
+ )
717
+ combined = " ".join(part for part in [server_name or "", name, description, param_text] if part)
718
+ tokens = self.tokenize(combined)
719
+
720
+ capabilities = self._match_labels(combined, self.CAPABILITY_PATTERNS)
721
+ consumes = self._infer_consumed_types(combined, params)
722
+ produces = self._infer_produced_types(combined, params, capabilities)
723
+ operations = self._infer_tool_operations(combined, capabilities)
724
+ constraints = self._infer_constraints(combined)
725
+ stage = self._infer_stage(capabilities, name, description)
726
+
727
+ curated = self._curated_semantics(server_name, name)
728
+ if curated:
729
+ if curated.get("override"):
730
+ capabilities = list(curated.get("capabilities", capabilities))
731
+ consumes = list(curated.get("consumes", consumes))
732
+ produces = list(curated.get("produces", produces))
733
+ operations = list(curated.get("operations", operations))
734
+ constraints = list(curated.get("constraints", constraints))
735
+ else:
736
+ capabilities = self._merge_labels(capabilities, curated.get("capabilities", []))
737
+ consumes = self._merge_labels(consumes, curated.get("consumes", []))
738
+ produces = self._merge_labels(produces, curated.get("produces", []))
739
+ operations = self._merge_labels(operations, curated.get("operations", []))
740
+ constraints = self._merge_labels(constraints, curated.get("constraints", []))
741
+ stage = curated.get("stage") or stage
742
+
743
+ operation_semantics = self._merge_operation_semantics(operations, consumes, produces, constraints)
744
+ consumes = operation_semantics["consumes"]
745
+ produces = operation_semantics["produces"]
746
+ constraints = operation_semantics["constraints"]
747
+
748
+ return {
749
+ "name": name,
750
+ "description": description,
751
+ "keywords": sorted(tokens),
752
+ "capabilities": capabilities,
753
+ "consumes": consumes,
754
+ "produces": produces,
755
+ "operations": operations,
756
+ "constraints": constraints,
757
+ "stage": stage,
758
+ "text": combined,
759
+ }
760
+
761
+ def extract_server_semantics(self, server_entry: dict) -> dict:
762
+ tool_semantics = [
763
+ self.extract_tool_semantics(tool, server_name=server_entry.get("name")) for tool in server_entry.get("tools", [])
764
+ ]
765
+ capability_counts = Counter()
766
+ datatype_counts = Counter()
767
+ operation_counts = Counter()
768
+ constraint_counts = Counter()
769
+ stage_counts = Counter()
770
+ keywords: set[str] = self.tokenize(
771
+ " ".join(
772
+ [
773
+ server_entry.get("name", ""),
774
+ server_entry.get("summary", ""),
775
+ " ".join(server_entry.get("keywords", [])),
776
+ ]
777
+ )
778
+ )
779
+ for tool in tool_semantics:
780
+ capability_counts.update(tool["capabilities"])
781
+ datatype_counts.update(tool["consumes"])
782
+ datatype_counts.update(tool["produces"])
783
+ operation_counts.update(tool.get("operations", []))
784
+ constraint_counts.update(tool.get("constraints", []))
785
+ if tool["stage"]:
786
+ stage_counts.update([tool["stage"]])
787
+ keywords.update(tool["keywords"])
788
+
789
+ capabilities = [item for item, _ in capability_counts.most_common()]
790
+ capabilities = self._sanitize_server_capabilities(server_entry, capabilities, tool_semantics)
791
+ stage_counts = Counter(
792
+ tool["stage"]
793
+ for tool in tool_semantics
794
+ if tool.get("stage") and any(cap in capabilities for cap in tool.get("capabilities", []))
795
+ )
796
+
797
+ return {
798
+ "capabilities": capabilities,
799
+ "datatypes": [item for item, _ in datatype_counts.most_common()],
800
+ "operations": [item for item, _ in operation_counts.most_common()],
801
+ "constraints": [item for item, _ in constraint_counts.most_common()],
802
+ "stages": [item for item, _ in stage_counts.most_common()],
803
+ "keywords": sorted(keywords),
804
+ "tool_semantics": tool_semantics,
805
+ }
806
+
807
+ def _curated_semantics(self, server_name: str | None, tool_name: str | None = None) -> dict:
808
+ if not server_name:
809
+ return {}
810
+ server_key = server_name.lower()
811
+ tool_key = (tool_name or "").lower()
812
+ if tool_key:
813
+ curated_tool = self.CURATED_TOOL_SEMANTICS.get((server_key, tool_key), {})
814
+ if curated_tool:
815
+ return curated_tool
816
+ curated = self.CURATED_SERVER_SEMANTICS.get(server_key, {})
817
+ if not curated and server_key.startswith("bioconductor-"):
818
+ curated = self.CURATED_SERVER_SEMANTICS.get(server_key.replace("_", "-"), {})
819
+ return curated
820
+
821
+ def _merge_labels(self, primary: list[str], additions: list[str]) -> list[str]:
822
+ merged = []
823
+ for item in list(primary or []) + list(additions or []):
824
+ if item and item not in merged:
825
+ merged.append(item)
826
+ return merged
827
+
828
+ def _sanitize_server_capabilities(
829
+ self,
830
+ server_entry: dict,
831
+ capabilities: list[str],
832
+ tool_semantics: list[dict],
833
+ ) -> list[str]:
834
+ server_name = str(server_entry.get("name", "")).lower()
835
+ category = str(server_entry.get("category", "")).lower()
836
+ combined_text = " ".join(
837
+ [
838
+ server_name,
839
+ str(server_entry.get("summary", "")),
840
+ " ".join(server_entry.get("keywords", [])),
841
+ " ".join(tool.get("text", "") for tool in tool_semantics),
842
+ ]
843
+ ).lower()
844
+
845
+ sanitized = list(capabilities)
846
+
847
+ if "database_search" in sanitized and not any(hint in combined_text for hint in self.DB_RESOURCE_HINTS):
848
+ sanitized = [cap for cap in sanitized if cap != "database_search"]
849
+
850
+ if "pathway_enrichment" in sanitized and not any(hint in combined_text for hint in self.PATHWAY_HINTS):
851
+ sanitized = [cap for cap in sanitized if cap != "pathway_enrichment"]
852
+
853
+ if "single_cell_analysis" in sanitized and not any(hint in combined_text for hint in self.SINGLE_CELL_HINTS):
854
+ sanitized = [cap for cap in sanitized if cap != "single_cell_analysis"]
855
+
856
+ if "differential_expression" in sanitized and not any(
857
+ hint in combined_text for hint in self.DIFFERENTIAL_EXPRESSION_HINTS
858
+ ):
859
+ sanitized = [cap for cap in sanitized if cap != "differential_expression"]
860
+
861
+ if category in {"single_cell", "transcriptomics"} and "database_search" in sanitized:
862
+ if not any(hint in combined_text for hint in {"cell atlas", "metadata query", "annotation database"}):
863
+ sanitized = [cap for cap in sanitized if cap != "database_search"]
864
+
865
+ allowed = self.SERVER_CAPABILITY_ALLOWLISTS.get(server_name)
866
+ if allowed:
867
+ sanitized = [cap for cap in sanitized if cap in allowed]
868
+
869
+ return sanitized
870
+
871
+ def infer_query_semantics(self, query_context: dict) -> dict:
872
+ text_parts = [
873
+ query_context.get("question", ""),
874
+ query_context.get("original_query", ""),
875
+ query_context.get("rewritten_query", ""),
876
+ query_context.get("retrieval_query", ""),
877
+ " ".join(query_context.get("subtasks", [])),
878
+ " ".join(query_context.get("keywords", [])),
879
+ " ".join(query_context.get("categories", [])),
880
+ ]
881
+ text = " ".join(part for part in text_parts if part)
882
+ capabilities = self._match_labels(text, self.CAPABILITY_PATTERNS)
883
+ datatypes = self._match_labels(text, self.DATATYPE_PATTERNS)
884
+ operations = self._infer_operations(text)
885
+ for operation in operations:
886
+ capabilities = self._merge_labels(capabilities, self.OPERATION_SPECS.get(operation, {}).get("capabilities", []))
887
+ stages = self._infer_query_stages(query_context, capabilities)
888
+ operation_tokens = {token for operation in operations for token in operation.split("_")}
889
+ constraints = self._infer_constraints(text)
890
+ task_spec = self.infer_task_spec(query_context, text, datatypes, operations, constraints)
891
+ operation_anchors = self._merge_labels(operations, task_spec.get("required_operations", []))
892
+ return {
893
+ "keywords": sorted(self.tokenize(text) | operation_tokens),
894
+ "capabilities": capabilities,
895
+ "datatypes": datatypes,
896
+ "operations": operations,
897
+ "operation_anchors": operation_anchors,
898
+ "stages": stages,
899
+ "organism": self._infer_organism(text),
900
+ "branch_labels": self._infer_branch_labels(text),
901
+ "workflow_type": self._infer_workflow_type(text, operations),
902
+ "output_schema": self._infer_output_schema(text),
903
+ "constraints": constraints,
904
+ "task_spec": task_spec,
905
+ }
906
+
907
+ def infer_task_spec(
908
+ self,
909
+ query_context: dict,
910
+ text: str,
911
+ datatypes: list[str],
912
+ operations: list[str],
913
+ constraints: list[str],
914
+ ) -> dict:
915
+ categories = {str(category).lower() for category in query_context.get("categories", [])}
916
+ capability_set = set(self._match_labels(text, self.CAPABILITY_PATTERNS))
917
+ datatype_set = set(datatypes)
918
+ operation_set = set(operations)
919
+ ranked_priors = rank_workflow_priors(
920
+ WORKFLOW_PRIORS,
921
+ text=text,
922
+ categories=categories,
923
+ capabilities=capability_set,
924
+ datatypes=datatype_set,
925
+ operations=operation_set,
926
+ )
927
+
928
+ input_types = list(datatypes)
929
+ target_output = {"semantic_type": "report_table"}
930
+ required_operations = list(operations)
931
+ assay = None
932
+
933
+ # These priors are graph-structured soft constraints. They expose
934
+ # operation anchors and preferred type signatures, but the concrete
935
+ # operation path is still selected later by constrained subgraph search.
936
+ for prior, _score in ranked_priors:
937
+ input_types = self._merge_labels(input_types, list(prior.input_types))
938
+ required_operations = self._merge_labels(required_operations, list(prior.operation_hints))
939
+ assay = assay or prior.assay
940
+
941
+ if ranked_priors:
942
+ best_prior, best_score = ranked_priors[0]
943
+ target_output = {"semantic_type": best_prior.target_output}
944
+ for prior, score in ranked_priors[1:]:
945
+ if prior.target_output == "report_table" and score >= best_score - 1.0:
946
+ target_output = {"semantic_type": "report_table"}
947
+ break
948
+
949
+ organism = self._infer_organism(text)
950
+ constraint_payload = {
951
+ "labels": constraints,
952
+ "organism": organism,
953
+ "assay": assay,
954
+ }
955
+ if organism:
956
+ constraint_payload["labels"] = self._merge_labels(constraint_payload["labels"], [f"organism_{organism}"])
957
+
958
+ return {
959
+ "input_types": input_types or ["text"],
960
+ "target_output": target_output,
961
+ "constraints": constraint_payload,
962
+ "required_operations": required_operations,
963
+ "workflow_type": self._infer_workflow_type(text, required_operations),
964
+ "workflow_priors": [
965
+ {"name": prior.name, "score": score, "target_output": prior.target_output}
966
+ for prior, score in ranked_priors
967
+ ],
968
+ }
969
+
970
+ def _match_labels(self, text: str, pattern_map: dict[str, set[str]]) -> list[str]:
971
+ lowered = (text or "").lower()
972
+ matched = []
973
+ for label, hints in pattern_map.items():
974
+ if any(hint in lowered for hint in hints):
975
+ matched.append(label)
976
+ return matched
977
+
978
+ def _infer_operations(self, text: str) -> list[str]:
979
+ lowered = (text or "").lower()
980
+ operations = []
981
+ for operation, spec in self.OPERATION_SPECS.items():
982
+ hints = spec.get("patterns", set())
983
+ if any(hint in lowered for hint in hints):
984
+ operations.append(operation)
985
+ return operations
986
+
987
+ def _infer_tool_operations(self, text: str, capabilities: list[str]) -> list[str]:
988
+ operations = set(self._infer_operations(text))
989
+ capability_set = set(capabilities)
990
+ capability_defaults = {
991
+ "quality_control": ["quality_control"],
992
+ "alignment": ["alignment"],
993
+ "quantification": ["quantification"],
994
+ "read_counting": ["read_counting"],
995
+ "normalization": ["count_normalization"],
996
+ "differential_expression": ["differential_expression"],
997
+ "database_search": ["database_lookup"],
998
+ "taxonomic_classification": ["taxonomic_classification"],
999
+ "single_cell_analysis": ["single_cell_clustering"],
1000
+ "variant_filtering": ["variant_annotation"],
1001
+ }
1002
+ for capability, defaults in capability_defaults.items():
1003
+ if capability in capability_set:
1004
+ operations.update(defaults)
1005
+ return sorted(operations)
1006
+
1007
+ def _infer_constraints(self, text: str) -> list[str]:
1008
+ lowered = (text or "").lower()
1009
+ constraints = []
1010
+ if any(token in lowered for token in ("mouse", "murine", "mus musculus")):
1011
+ constraints.append("organism_mouse")
1012
+ if any(token in lowered for token in ("human", "homo sapiens")):
1013
+ constraints.append("organism_human")
1014
+ if any(token in lowered for token in ("bulk rna", "bulk rna-seq", "deseq2", "raw counts")):
1015
+ constraints.append("bulk_rna_seq")
1016
+ if any(token in lowered for token in ("integer count", "raw count", "raw counts")):
1017
+ constraints.append("requires_raw_integer_counts")
1018
+ if "entrez" in lowered:
1019
+ constraints.append("requires_entrez_id")
1020
+ if any(token in lowered for token in ("replicate", "replicates")):
1021
+ constraints.append("requires_replicates")
1022
+ if any(token in lowered for token in ("paired-end", "paired end", "read1", "read2")):
1023
+ constraints.append("paired_end_reads")
1024
+ return constraints
1025
+
1026
+ def _merge_operation_semantics(
1027
+ self,
1028
+ operations: list[str],
1029
+ consumes: list[str],
1030
+ produces: list[str],
1031
+ constraints: list[str],
1032
+ ) -> dict:
1033
+ merged_consumes = list(consumes)
1034
+ merged_produces = list(produces)
1035
+ merged_constraints = list(constraints)
1036
+ for operation in operations:
1037
+ spec = self.OPERATION_SPECS.get(operation, {})
1038
+ merged_consumes = self._merge_labels(merged_consumes, spec.get("accepts", []))
1039
+ merged_produces = self._merge_labels(merged_produces, spec.get("produces", []))
1040
+ merged_constraints = self._merge_labels(merged_constraints, spec.get("constraints", []))
1041
+ return {
1042
+ "consumes": merged_consumes,
1043
+ "produces": merged_produces,
1044
+ "constraints": merged_constraints,
1045
+ }
1046
+
1047
+ def _infer_organism(self, text: str) -> str | None:
1048
+ lowered = (text or "").lower()
1049
+ if any(token in lowered for token in ("mouse", "murine", "mus musculus")):
1050
+ return "mouse"
1051
+ if any(token in lowered for token in ("human", "homo sapiens")):
1052
+ return "human"
1053
+ return None
1054
+
1055
+ def _infer_branch_labels(self, text: str) -> list[str]:
1056
+ labels = []
1057
+ for pattern in (r"\b5xfad\b", r"\b3xtg[-_ ]?ad\b", r"\bps?[30o][0o]1s\b"):
1058
+ for match in re.findall(pattern, text or "", flags=re.IGNORECASE):
1059
+ normalized = match.replace(" ", "-")
1060
+ if normalized.lower() == "3xtg-ad":
1061
+ normalized = "3xTG-AD"
1062
+ elif normalized.lower() == "5xfad":
1063
+ normalized = "5xFAD"
1064
+ elif normalized.lower() in {"ps301s", "ps3o1s", "p301s", "p3o1s"}:
1065
+ normalized = "PS301S"
1066
+ if normalized not in labels:
1067
+ labels.append(normalized)
1068
+ return labels
1069
+
1070
+ def _infer_workflow_type(self, text: str, operations: list[str]) -> str:
1071
+ lowered = (text or "").lower()
1072
+ has_comparison = any(token in lowered for token in ("compare", "comparative", "shared", "across"))
1073
+ if has_comparison and len(self._infer_branch_labels(text)) >= 2:
1074
+ return "fork_join_comparative_analysis"
1075
+ if len(operations) > 1:
1076
+ return "linear_typed_transformation"
1077
+ return "single_step"
1078
+
1079
+ def _infer_output_schema(self, text: str) -> dict:
1080
+ lowered = (text or "").lower()
1081
+ if "pathway" in lowered and any(token in lowered for token in ("5xfad", "3xtg", "ps301s", "p301s")):
1082
+ return {
1083
+ "columns": ["pathway", "5xFAD_pvalue", "3xTG_AD_pvalue", "PS3O1S_pvalue"],
1084
+ "join_key": "pathway",
1085
+ }
1086
+ if "csv" in lowered:
1087
+ return {"format": "csv"}
1088
+ return {}
1089
+
1090
+ def _infer_consumed_types(self, text: str, params: dict) -> list[str]:
1091
+ lowered = (text or "").lower()
1092
+ matched = set(self._match_labels(lowered, self.DATATYPE_PATTERNS))
1093
+ for param_name, spec in params.items():
1094
+ blob = f"{param_name} {spec.get('description', '')}".lower()
1095
+ matched.update(self._match_labels(blob, self.DATATYPE_PATTERNS))
1096
+ if any(keyword in param_name.lower() for keyword in ("input", "file", "path", "filename")):
1097
+ matched.update(self._match_labels(blob, self.DATATYPE_PATTERNS))
1098
+ return sorted(matched)
1099
+
1100
+ def _infer_produced_types(self, text: str, params: dict, capabilities: list[str]) -> list[str]:
1101
+ lowered = (text or "").lower()
1102
+ produced = set()
1103
+ for label, hints in self.DATATYPE_PATTERNS.items():
1104
+ if any(hint in lowered for hint in hints) and any(
1105
+ marker in lowered for marker in ("save", "write", "export", "output", "generate", "produce")
1106
+ ):
1107
+ produced.add(label)
1108
+ for param_name, spec in params.items():
1109
+ blob = f"{param_name} {spec.get('description', '')}".lower()
1110
+ if any(keyword in param_name.lower() for keyword in ("output", "save", "dest", "dir")):
1111
+ produced.update(self._match_labels(blob, self.DATATYPE_PATTERNS))
1112
+ if "pathway_enrichment" in capabilities:
1113
+ produced.add("pathway_table")
1114
+ if "differential_expression" in capabilities:
1115
+ produced.add("de_table")
1116
+ if "visualization" in capabilities:
1117
+ produced.add("image")
1118
+ return sorted(produced)
1119
+
1120
+ def _infer_stage(self, capabilities: list[str], name: str, description: str) -> str:
1121
+ lowered = f"{name} {description}".lower()
1122
+ if any(token in lowered for token in ("plot", "figure", "visual")):
1123
+ return "reporting"
1124
+ # Prefer later analytical stages over generic preprocessing hints.
1125
+ priority_rules = [
1126
+ ("downstream", {"pathway_enrichment", "annotation", "genome_annotation"}),
1127
+ (
1128
+ "analysis",
1129
+ {
1130
+ "quantification",
1131
+ "read_counting",
1132
+ "differential_expression",
1133
+ "single_cell_analysis",
1134
+ "variant_calling",
1135
+ "variant_filtering",
1136
+ "taxonomic_classification",
1137
+ "orthology_clustering",
1138
+ "statistics",
1139
+ },
1140
+ ),
1141
+ ("preprocessing", {"data_inspection", "format_conversion", "quality_control", "alignment", "normalization"}),
1142
+ ("input_acquisition", {"database_search"}),
1143
+ ]
1144
+ for stage, stage_capabilities in priority_rules:
1145
+ if any(capability in stage_capabilities for capability in capabilities):
1146
+ return stage
1147
+ for stage, stage_capabilities in self.STAGE_RULES:
1148
+ if any(capability in stage_capabilities for capability in capabilities):
1149
+ return stage
1150
+ return "analysis"
1151
+
1152
+ def _infer_query_stages(self, query_context: dict, capabilities: list[str]) -> list[str]:
1153
+ text = " ".join(query_context.get("subtasks", [])) + " " + query_context.get("rewritten_query", "")
1154
+ lowered = text.lower()
1155
+ stages = []
1156
+ if any(token in lowered for token in ("inspect", "load", "schema", "preview", "normalize", "normalise")):
1157
+ stages.append("preprocessing")
1158
+ if any(
1159
+ token in lowered
1160
+ for token in (
1161
+ "analyze",
1162
+ "differential",
1163
+ "compare",
1164
+ "cluster",
1165
+ "quantify",
1166
+ "count",
1167
+ "variant",
1168
+ "taxonomy",
1169
+ )
1170
+ ):
1171
+ stages.append("analysis")
1172
+ if any(token in lowered for token in ("gsea", "enrichment", "pathway")):
1173
+ stages.append("downstream")
1174
+ if any(token in lowered for token in ("summarize", "report", "save plot", "visualize")):
1175
+ stages.append("reporting")
1176
+ if not stages:
1177
+ for stage, stage_capabilities in self.STAGE_RULES:
1178
+ if any(capability in stage_capabilities for capability in capabilities):
1179
+ stages.append(stage)
1180
+ return stages
Biomanus_upload/biomni/graph/tool_graph.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+
5
+ from biomni.graph.schema_extractor import ToolSchemaExtractor
6
+
7
+
8
+ class ToolGraph:
9
+ """A lightweight heterogeneous graph over MCP servers, tools, capabilities, and data types."""
10
+
11
+ def __init__(self, schema_extractor: ToolSchemaExtractor | None = None):
12
+ self.schema_extractor = schema_extractor or ToolSchemaExtractor()
13
+ self.clear()
14
+
15
+ def clear(self) -> None:
16
+ self.nodes: dict[str, dict] = {}
17
+ self.edges: list[dict] = []
18
+ self.adjacency: dict[str, list[dict]] = defaultdict(list)
19
+ self.server_entries: dict[str, dict] = {}
20
+ self.server_index: dict[str, dict] = {}
21
+ self.tool_index: dict[str, dict] = {}
22
+ self.operation_index: dict[str, dict] = {}
23
+ self.datatype_to_operations: dict[str, list[str]] = defaultdict(list)
24
+
25
+ def add_node(self, node_id: str, node_type: str, label: str, **attributes) -> None:
26
+ payload = {"id": node_id, "type": node_type, "label": label}
27
+ payload.update(attributes)
28
+ self.nodes[node_id] = payload
29
+
30
+ def add_edge(self, source: str, target: str, edge_type: str, weight: float = 1.0, **attributes) -> None:
31
+ payload = {"source": source, "target": target, "type": edge_type, "weight": weight}
32
+ payload.update(attributes)
33
+ self.edges.append(payload)
34
+ self.adjacency[source].append(payload)
35
+
36
+ def build_from_server_entries(self, server_entries: list[dict]) -> None:
37
+ self.clear()
38
+ for entry in server_entries:
39
+ self._add_server(entry)
40
+ self._add_cross_server_workflow_edges()
41
+
42
+ def get_server_entry(self, server_name: str) -> dict | None:
43
+ return self.server_entries.get(server_name)
44
+
45
+ def get_server_names(self) -> list[str]:
46
+ return list(self.server_entries.keys())
47
+
48
+ def get_tool_nodes_for_server(self, server_name: str) -> list[dict]:
49
+ return self.server_index.get(server_name, {}).get("tools", [])
50
+
51
+ def get_server_semantics(self, server_name: str) -> dict:
52
+ return self.server_index.get(server_name, {}).get("semantics", {})
53
+
54
+ def get_server_neighbors(self, server_name: str, max_hops: int = 2) -> list[dict]:
55
+ start_node = f"server:{server_name}"
56
+ if start_node not in self.nodes:
57
+ return []
58
+ visited = {start_node}
59
+ frontier = {start_node}
60
+ collected = []
61
+ for _ in range(max_hops):
62
+ next_frontier = set()
63
+ for node_id in frontier:
64
+ for edge in self.adjacency.get(node_id, []):
65
+ if edge["target"] in visited:
66
+ continue
67
+ visited.add(edge["target"])
68
+ next_frontier.add(edge["target"])
69
+ collected.append(self.nodes[edge["target"]])
70
+ frontier = next_frontier
71
+ if not frontier:
72
+ break
73
+ return collected
74
+
75
+ def _add_server(self, entry: dict) -> None:
76
+ server_name = entry["name"]
77
+ server_node = f"server:{server_name}"
78
+ semantics = self.schema_extractor.extract_server_semantics(entry)
79
+
80
+ self.add_node(
81
+ server_node,
82
+ "server",
83
+ server_name,
84
+ category=entry.get("category", "general"),
85
+ summary=entry.get("summary", ""),
86
+ keywords=semantics.get("keywords", []),
87
+ )
88
+ self.server_entries[server_name] = entry
89
+ self.server_index[server_name] = {"entry": entry, "semantics": semantics, "tools": []}
90
+
91
+ category = entry.get("category")
92
+ if category:
93
+ category_node = f"category:{category}"
94
+ self.add_node(category_node, "category", category)
95
+ self.add_edge(server_node, category_node, "categorized_as", weight=1.0)
96
+
97
+ for tool, tool_semantics in zip(entry.get("tools", []), semantics.get("tool_semantics", []), strict=False):
98
+ tool_name = tool_semantics["name"]
99
+ tool_node = f"tool:{server_name}:{tool_name}"
100
+ self.add_node(
101
+ tool_node,
102
+ "tool",
103
+ tool_name,
104
+ description=tool_semantics.get("description", ""),
105
+ keywords=tool_semantics.get("keywords", []),
106
+ module=entry.get("module"),
107
+ )
108
+ self.add_edge(server_node, tool_node, "hosts", weight=3.0)
109
+ self.server_index[server_name]["tools"].append({"node_id": tool_node, **tool, **tool_semantics})
110
+ self.tool_index[tool_node] = {"server_name": server_name, "tool": tool, "semantics": tool_semantics}
111
+
112
+ stage = tool_semantics.get("stage")
113
+ if stage:
114
+ stage_node = f"stage:{stage}"
115
+ self.add_node(stage_node, "stage", stage)
116
+ self.add_edge(tool_node, stage_node, "belongs_to_stage", weight=1.0)
117
+
118
+ for capability in tool_semantics.get("capabilities", []):
119
+ capability_node = f"capability:{capability}"
120
+ self.add_node(capability_node, "capability", capability)
121
+ self.add_edge(tool_node, capability_node, "implements", weight=2.0)
122
+
123
+ for operation in tool_semantics.get("operations", []):
124
+ self._add_operation_binding(operation, tool_node, tool_semantics)
125
+
126
+ for data_type in tool_semantics.get("consumes", []):
127
+ datatype_node = f"datatype:{data_type}"
128
+ self.add_node(datatype_node, "datatype", data_type)
129
+ self.add_edge(tool_node, datatype_node, "consumes", weight=2.0)
130
+
131
+ for data_type in tool_semantics.get("produces", []):
132
+ datatype_node = f"datatype:{data_type}"
133
+ self.add_node(datatype_node, "datatype", data_type)
134
+ self.add_edge(tool_node, datatype_node, "produces", weight=2.0)
135
+
136
+ for constraint in tool_semantics.get("constraints", []):
137
+ constraint_node = f"constraint:{constraint}"
138
+ self.add_node(constraint_node, "constraint", constraint)
139
+ self.add_edge(tool_node, constraint_node, "supports", weight=1.5)
140
+
141
+ self._add_workflow_edges(server_name)
142
+
143
+ def _add_operation_binding(self, operation: str, tool_node: str, tool_semantics: dict) -> None:
144
+ operation_node = f"operation:{operation}"
145
+ spec = self.schema_extractor.OPERATION_SPECS.get(operation, {})
146
+ accepts = spec.get("accepts", []) or tool_semantics.get("consumes", [])
147
+ produces = spec.get("produces", []) or tool_semantics.get("produces", [])
148
+ constraints = spec.get("constraints", []) or tool_semantics.get("constraints", [])
149
+
150
+ self.add_node(
151
+ operation_node,
152
+ "operation",
153
+ operation,
154
+ stage=spec.get("stage") or tool_semantics.get("stage"),
155
+ accepts=accepts,
156
+ produces=produces,
157
+ constraints=constraints,
158
+ )
159
+ self.add_edge(tool_node, operation_node, "implements_operation", weight=3.0)
160
+
161
+ entry = self.operation_index.setdefault(
162
+ operation,
163
+ {
164
+ "operation": operation,
165
+ "node_id": operation_node,
166
+ "accepts": [],
167
+ "produces": [],
168
+ "constraints": [],
169
+ "tools": [],
170
+ "stage": spec.get("stage") or tool_semantics.get("stage") or "analysis",
171
+ },
172
+ )
173
+ entry["accepts"] = self._merge_labels(entry["accepts"], accepts)
174
+ entry["produces"] = self._merge_labels(entry["produces"], produces)
175
+ entry["constraints"] = self._merge_labels(entry["constraints"], constraints)
176
+ entry["tools"].append({**tool_semantics, "node_id": tool_node})
177
+
178
+ for data_type in accepts:
179
+ datatype_node = f"datatype:{data_type}"
180
+ self.add_node(datatype_node, "datatype", data_type)
181
+ self.add_edge(operation_node, datatype_node, "accepts", weight=2.0)
182
+ if operation not in self.datatype_to_operations[data_type]:
183
+ self.datatype_to_operations[data_type].append(operation)
184
+
185
+ for data_type in produces:
186
+ datatype_node = f"datatype:{data_type}"
187
+ self.add_node(datatype_node, "datatype", data_type)
188
+ self.add_edge(operation_node, datatype_node, "produces", weight=2.0)
189
+
190
+ for constraint in constraints:
191
+ constraint_node = f"constraint:{constraint}"
192
+ self.add_node(constraint_node, "constraint", constraint)
193
+ self.add_edge(operation_node, constraint_node, "requires", weight=1.5)
194
+
195
+ def _merge_labels(self, primary: list[str], additions: list[str]) -> list[str]:
196
+ merged = list(primary or [])
197
+ for item in additions or []:
198
+ if item and item not in merged:
199
+ merged.append(item)
200
+ return merged
201
+
202
+ def _add_workflow_edges(self, server_name: str) -> None:
203
+ tools = self.server_index.get(server_name, {}).get("tools", [])
204
+ for source in tools:
205
+ source_produced = set(source.get("produces", []))
206
+ source_stage = source.get("stage")
207
+ for target in tools:
208
+ if source["name"] == target["name"]:
209
+ continue
210
+ target_consumed = set(target.get("consumes", []))
211
+ if source_produced and target_consumed and source_produced & target_consumed:
212
+ self.add_edge(
213
+ source["node_id"],
214
+ target["node_id"],
215
+ "follows",
216
+ weight=1.5,
217
+ shared_datatypes=sorted(source_produced & target_consumed),
218
+ )
219
+ elif source_stage and target.get("stage") and self._stage_distance(source_stage, target["stage"]) == 1:
220
+ self.add_edge(
221
+ source["node_id"],
222
+ target["node_id"],
223
+ "adjacent_stage",
224
+ weight=0.5,
225
+ )
226
+
227
+ def _add_cross_server_workflow_edges(self) -> None:
228
+ consume_index: dict[str, list[dict]] = defaultdict(list)
229
+ all_tools = []
230
+ for server_name in self.server_index:
231
+ for tool in self.server_index[server_name].get("tools", []):
232
+ all_tools.append(tool)
233
+ for data_type in tool.get("consumes", []):
234
+ consume_index[data_type].append(tool)
235
+
236
+ generic_types = {"text", "image", "json", "csv"}
237
+ for source in all_tools:
238
+ produced_types = set(source.get("produces", [])) - generic_types
239
+ if not produced_types:
240
+ continue
241
+
242
+ candidates = []
243
+ for data_type in produced_types:
244
+ for target in consume_index.get(data_type, []):
245
+ if source["node_id"] == target["node_id"]:
246
+ continue
247
+ if source["node_id"].split(":")[1] == target["node_id"].split(":")[1]:
248
+ continue
249
+ stage_delta = self._stage_distance(source.get("stage"), target.get("stage"))
250
+ if stage_delta < 0 or stage_delta > 2:
251
+ continue
252
+ candidates.append((stage_delta, data_type, target))
253
+
254
+ candidates.sort(key=lambda item: (item[0], item[2].get("name", "")))
255
+ seen_targets = set()
256
+ for stage_delta, data_type, target in candidates[:12]:
257
+ if target["node_id"] in seen_targets:
258
+ continue
259
+ seen_targets.add(target["node_id"])
260
+ self.add_edge(
261
+ source["node_id"],
262
+ target["node_id"],
263
+ "typed_flow",
264
+ weight=2.5 if stage_delta <= 1 else 1.5,
265
+ shared_datatypes=[data_type],
266
+ cross_server=True,
267
+ )
268
+
269
+ def _stage_distance(self, source_stage: str, target_stage: str) -> int:
270
+ order = ["input_acquisition", "preprocessing", "analysis", "downstream", "reporting"]
271
+ if source_stage not in order or target_stage not in order:
272
+ return 99
273
+ return order.index(target_stage) - order.index(source_stage)
Biomanus_upload/biomni/graph/workflow_priors.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Iterable
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class WorkflowPrior:
9
+ """A graph-structured workflow prior, not a task-specific pipeline.
10
+
11
+ Priors describe soft evidence that a query belongs near a region of the
12
+ heterogeneous tool graph. They provide semantic anchors and preferred
13
+ datatypes, while the final operation path is still selected dynamically by
14
+ graph search over typed-flow and stage-adjacency structure.
15
+ """
16
+
17
+ name: str
18
+ categories: frozenset[str] = frozenset()
19
+ capabilities: frozenset[str] = frozenset()
20
+ datatypes: frozenset[str] = frozenset()
21
+ keyword_hints: frozenset[str] = frozenset()
22
+ operation_hints: tuple[str, ...] = ()
23
+ input_types: tuple[str, ...] = ()
24
+ target_output: str = "report_table"
25
+ assay: str | None = None
26
+ min_score: float = 4.0
27
+ weight: float = 1.0
28
+
29
+ def score(
30
+ self,
31
+ *,
32
+ text: str,
33
+ categories: set[str],
34
+ capabilities: set[str],
35
+ datatypes: set[str],
36
+ operations: set[str],
37
+ ) -> float:
38
+ lowered = (text or "").lower()
39
+ score = 0.0
40
+ score += 4.0 * len(self.categories & categories)
41
+ score += 2.0 * len(self.capabilities & capabilities)
42
+ score += 1.5 * len(self.datatypes & datatypes)
43
+ score += 2.0 * len(set(self.operation_hints) & operations)
44
+ score += sum(1.0 for hint in self.keyword_hints if hint in lowered)
45
+ return score * self.weight
46
+
47
+
48
+ WORKFLOW_PRIORS: tuple[WorkflowPrior, ...] = (
49
+ WorkflowPrior(
50
+ name="database_grounding",
51
+ categories=frozenset({"database_question_answering"}),
52
+ capabilities=frozenset({"database_search", "annotation"}),
53
+ keyword_hints=frozenset({"database", "clinvar", "ensembl", "uniprot", "disgenet", "omim", "mirdb", "gtrd"}),
54
+ operation_hints=("database_lookup",),
55
+ input_types=("text",),
56
+ target_output="database_record",
57
+ ),
58
+ WorkflowPrior(
59
+ name="sequence_feature_reasoning",
60
+ categories=frozenset({"sequence_question_answering"}),
61
+ capabilities=frozenset({"alignment", "annotation", "format_conversion"}),
62
+ datatypes=frozenset({"fastq", "reference_genome", "sequence_feature"}),
63
+ keyword_hints=frozenset({"sequence", "fasta", "motif", "primer", "binding site", "reverse complement"}),
64
+ operation_hints=("sequence_feature_extraction",),
65
+ input_types=("text",),
66
+ target_output="sequence_feature",
67
+ ),
68
+ WorkflowPrior(
69
+ name="bulk_expression_analysis",
70
+ capabilities=frozenset({"differential_expression", "normalization", "statistics", "read_counting"}),
71
+ datatypes=frozenset({"raw_count_matrix", "normalized_count_matrix", "sample_metadata", "differential_expression_table"}),
72
+ keyword_hints=frozenset({"rna-seq", "bulk", "count matrix", "raw counts", "deseq2", "log2fc", "padj"}),
73
+ operation_hints=("differential_expression",),
74
+ input_types=("raw_count_matrix", "sample_metadata"),
75
+ target_output="differential_expression_table",
76
+ assay="bulk_rna_seq",
77
+ ),
78
+ WorkflowPrior(
79
+ name="pathway_interpretation",
80
+ capabilities=frozenset({"pathway_enrichment", "database_search", "format_conversion"}),
81
+ datatypes=frozenset({"differential_expression_table", "gene_symbol_list", "entrez_gene_list", "kegg_enrichment_table"}),
82
+ keyword_hints=frozenset({"pathway", "kegg", "enrichment", "gsea", "reactome", "gene ontology"}),
83
+ operation_hints=("gene_filtering", "gene_id_conversion", "kegg_enrichment"),
84
+ input_types=("differential_expression_table",),
85
+ target_output="kegg_enrichment_table",
86
+ ),
87
+ WorkflowPrior(
88
+ name="comparative_result_synthesis",
89
+ capabilities=frozenset({"format_conversion", "statistics"}),
90
+ datatypes=frozenset({"kegg_enrichment_table", "merged_pathway_table", "csv"}),
91
+ keyword_hints=frozenset({"compare", "comparative", "shared", "across", "merge", "final table", "csv"}),
92
+ operation_hints=("pathway_merge", "csv_export"),
93
+ input_types=("kegg_enrichment_table",),
94
+ target_output="report_table",
95
+ ),
96
+ WorkflowPrior(
97
+ name="variant_interpretation",
98
+ capabilities=frozenset({"variant_calling", "variant_filtering", "annotation"}),
99
+ datatypes=frozenset({"vcf", "annotated_variant_table", "variant_summary"}),
100
+ keyword_hints=frozenset({"variant", "vcf", "mutation", "clinvar", "pathogenic"}),
101
+ operation_hints=("variant_annotation", "variant_summary"),
102
+ input_types=("vcf",),
103
+ target_output="variant_summary",
104
+ ),
105
+ WorkflowPrior(
106
+ name="single_cell_interpretation",
107
+ capabilities=frozenset({"single_cell_analysis", "annotation", "statistics"}),
108
+ datatypes=frozenset({"anndata", "marker_gene_table"}),
109
+ keyword_hints=frozenset({"single-cell", "singlecell", "scrna", "cluster", "cell type", "marker"}),
110
+ operation_hints=("single_cell_clustering", "marker_gene_detection"),
111
+ input_types=("anndata",),
112
+ target_output="marker_gene_table",
113
+ ),
114
+ WorkflowPrior(
115
+ name="taxonomic_profiling",
116
+ capabilities=frozenset({"taxonomic_classification"}),
117
+ datatypes=frozenset({"metagenomic_reads", "taxonomic_profile"}),
118
+ keyword_hints=frozenset({"metagenomic", "metagenomics", "taxonomy", "taxonomic", "kraken", "metaphlan"}),
119
+ operation_hints=("taxonomic_classification",),
120
+ input_types=("metagenomic_reads",),
121
+ target_output="taxonomic_profile",
122
+ ),
123
+ )
124
+
125
+
126
+ def rank_workflow_priors(
127
+ priors: Iterable[WorkflowPrior],
128
+ *,
129
+ text: str,
130
+ categories: set[str],
131
+ capabilities: set[str],
132
+ datatypes: set[str],
133
+ operations: set[str],
134
+ ) -> list[tuple[WorkflowPrior, float]]:
135
+ ranked = []
136
+ for prior in priors:
137
+ score = prior.score(
138
+ text=text,
139
+ categories=categories,
140
+ capabilities=capabilities,
141
+ datatypes=datatypes,
142
+ operations=operations,
143
+ )
144
+ if score >= prior.min_score:
145
+ ranked.append((prior, score))
146
+ ranked.sort(key=lambda item: (-item[1], item[0].name))
147
+ return ranked
Biomanus_upload/biomni/know_how/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """Know-How library for Biomni agent.
2
+
3
+ This module provides functionality to load and manage know-how documents
4
+ that contain best practices, protocols, and troubleshooting guides.
5
+ """
6
+
7
+ from .loader import KnowHowLoader
8
+
9
+ __all__ = ["KnowHowLoader"]
Biomanus_upload/biomni/know_how/loader.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Loader for know-how documents."""
2
+
3
+ import glob
4
+ import os
5
+ from pathlib import Path
6
+
7
+
8
+ class KnowHowLoader:
9
+ """Load and manage know-how documents for the agent."""
10
+
11
+ def __init__(self, know_how_dir: str | None = None):
12
+ """Initialize the know-how loader.
13
+
14
+ Args:
15
+ know_how_dir: Directory containing know-how documents.
16
+ If None, uses the default know_how directory.
17
+
18
+ """
19
+ if know_how_dir is None:
20
+ # Default to the know_how directory in the package
21
+ current_dir = Path(__file__).parent
22
+ know_how_dir = str(current_dir)
23
+
24
+ self.know_how_dir = know_how_dir
25
+ self.documents = {}
26
+ self._load_documents()
27
+
28
+ def _load_documents(self):
29
+ """Load all markdown documents from the know-how directory."""
30
+ pattern = os.path.join(self.know_how_dir, "*.md")
31
+ md_files = glob.glob(pattern)
32
+
33
+ for filepath in md_files:
34
+ filename = os.path.basename(filepath)
35
+ filename_without_ext = os.path.splitext(filename)[0]
36
+
37
+ # Skip README, QUICK_START, and other meta documentation (all caps filenames)
38
+ if filename.upper() in ["README.MD", "QUICK_START.MD"] or filename_without_ext.isupper():
39
+ continue
40
+
41
+ # Read the document
42
+ with open(filepath) as f:
43
+ content = f.read()
44
+
45
+ # Extract title, description, and metadata from the document
46
+ title, description, metadata = self._extract_metadata(content, filename)
47
+
48
+ # Use short_description from metadata if available, otherwise fall back to extracted description
49
+ if "short_description" in metadata and metadata["short_description"]:
50
+ description = metadata["short_description"]
51
+
52
+ # Store document info
53
+ doc_id = os.path.splitext(filename)[0]
54
+ self.documents[doc_id] = {
55
+ "id": doc_id,
56
+ "name": title,
57
+ "description": description,
58
+ "content": content,
59
+ "content_without_metadata": self._strip_metadata(content),
60
+ "filepath": filepath,
61
+ "metadata": metadata,
62
+ }
63
+
64
+ def _extract_metadata(self, content: str, filename: str) -> tuple[str, str, dict]:
65
+ """Extract title, description, and metadata from markdown content.
66
+
67
+ Args:
68
+ content: Markdown content
69
+ filename: Filename (used as fallback for title)
70
+
71
+ Returns:
72
+ Tuple of (title, description, metadata_dict)
73
+
74
+ """
75
+ lines = content.split("\n")
76
+
77
+ # Extract title (first h1)
78
+ title = None
79
+ for line in lines:
80
+ if line.startswith("# "):
81
+ title = line[2:].strip()
82
+ break
83
+
84
+ if title is None:
85
+ # Fallback to filename
86
+ title = filename.replace("_", " ").replace(".md", "").title()
87
+
88
+ # Extract metadata section
89
+ metadata = {}
90
+ in_metadata = False
91
+ current_field = None
92
+
93
+ for _i, line in enumerate(lines):
94
+ if line.startswith("## Metadata"):
95
+ in_metadata = True
96
+ continue
97
+ elif in_metadata:
98
+ if line.startswith("##") and "Metadata" not in line:
99
+ # End of metadata section
100
+ break
101
+ elif line.startswith("**") and "**:" in line:
102
+ # New metadata field (e.g., **Authors**:)
103
+ field_match = line.split("**")[1]
104
+ current_field = field_match.lower().replace(" ", "_")
105
+ # Get the value after the colon if it exists on the same line
106
+ colon_idx = line.find("**:")
107
+ if colon_idx != -1:
108
+ value_part = line[colon_idx + 3 :].strip()
109
+ if value_part:
110
+ metadata[current_field] = value_part
111
+ else:
112
+ metadata[current_field] = ""
113
+ elif current_field and line.strip() and not line.startswith("---"):
114
+ # Continuation of current field
115
+ if current_field not in metadata:
116
+ metadata[current_field] = ""
117
+ if line.startswith("- "):
118
+ # List item
119
+ if metadata[current_field]:
120
+ metadata[current_field] += ", " + line[2:].strip()
121
+ else:
122
+ metadata[current_field] = line[2:].strip()
123
+ elif not line.startswith("```"):
124
+ # Regular text
125
+ if metadata[current_field]:
126
+ metadata[current_field] += " " + line.strip()
127
+ else:
128
+ metadata[current_field] = line.strip()
129
+
130
+ # Extract description (content under ## Overview or first paragraph)
131
+ description = ""
132
+ in_overview = False
133
+ overview_lines = []
134
+
135
+ for _i, line in enumerate(lines):
136
+ if line.startswith("## Overview"):
137
+ in_overview = True
138
+ continue
139
+ elif in_overview:
140
+ if line.startswith("##"):
141
+ # End of overview section
142
+ break
143
+ elif line.strip():
144
+ overview_lines.append(line.strip())
145
+
146
+ if overview_lines:
147
+ description = " ".join(overview_lines)
148
+ else:
149
+ # Fallback: get first non-empty paragraph after title
150
+ found_title = False
151
+ for line in lines:
152
+ if line.startswith("# "):
153
+ found_title = True
154
+ continue
155
+ if found_title and line.strip() and not line.startswith("#"):
156
+ description = line.strip()
157
+ break
158
+
159
+ # Limit description length
160
+ if len(description) > 200:
161
+ description = description[:197] + "..."
162
+
163
+ return title, description, metadata
164
+
165
+ def _strip_metadata(self, content: str) -> str:
166
+ """Strip the metadata section from document content.
167
+
168
+ Args:
169
+ content: Full document content with metadata
170
+
171
+ Returns:
172
+ Content without metadata section
173
+
174
+ """
175
+ lines = content.split("\n")
176
+ result_lines = []
177
+ in_metadata = False
178
+ skip_until_separator = False
179
+ found_first_h1 = False
180
+
181
+ for line in lines:
182
+ # Track first H1 (title)
183
+ if line.startswith("# ") and not found_first_h1:
184
+ result_lines.append(line)
185
+ found_first_h1 = True
186
+ continue
187
+
188
+ # Detect metadata section start
189
+ if line.startswith("## Metadata"):
190
+ in_metadata = True
191
+ continue
192
+
193
+ # Skip separator lines before and after metadata
194
+ if line.strip() == "---":
195
+ if not in_metadata:
196
+ # This might be the separator before metadata
197
+ skip_until_separator = True
198
+ continue
199
+ else:
200
+ # This is the separator after metadata
201
+ in_metadata = False
202
+ skip_until_separator = False
203
+ continue
204
+
205
+ # Skip lines in metadata section
206
+ if in_metadata or skip_until_separator:
207
+ # Check if we hit another H2 (end of metadata)
208
+ if line.startswith("##") and "Metadata" not in line:
209
+ in_metadata = False
210
+ skip_until_separator = False
211
+ result_lines.append(line)
212
+ continue
213
+
214
+ # Keep all other lines
215
+ result_lines.append(line)
216
+
217
+ # Join and clean up extra blank lines
218
+ result = "\n".join(result_lines)
219
+
220
+ # Remove excessive blank lines (more than 2 consecutive)
221
+ while "\n\n\n\n" in result:
222
+ result = result.replace("\n\n\n\n", "\n\n\n")
223
+
224
+ return result.strip()
225
+
226
+ def get_all_documents(self) -> list[dict]:
227
+ """Get all know-how documents as a list.
228
+
229
+ Returns:
230
+ List of document dictionaries with keys: id, name, description, content
231
+
232
+ """
233
+ return list(self.documents.values())
234
+
235
+ def get_document_by_id(self, doc_id: str) -> dict | None:
236
+ """Get a specific know-how document by ID.
237
+
238
+ Args:
239
+ doc_id: Document identifier
240
+
241
+ Returns:
242
+ Document dictionary or None if not found
243
+
244
+ """
245
+ return self.documents.get(doc_id)
246
+
247
+ def get_document_summaries(self) -> list[dict]:
248
+ """Get summaries of all documents (without full content).
249
+
250
+ Returns:
251
+ List of document summaries with keys: id, name, description
252
+
253
+ """
254
+ return [
255
+ {"id": doc["id"], "name": doc["name"], "description": doc["description"]} for doc in self.documents.values()
256
+ ]
257
+
258
+ def add_custom_document(self, doc_id: str, name: str, description: str, content: str, metadata: dict | None = None):
259
+ """Add a custom know-how document programmatically.
260
+
261
+ Args:
262
+ doc_id: Unique identifier for the document
263
+ name: Document title
264
+ description: Brief description
265
+ content: Full document content
266
+ metadata: Optional metadata dictionary (authors, license, etc.)
267
+
268
+ """
269
+ if metadata is None:
270
+ metadata = {}
271
+
272
+ self.documents[doc_id] = {
273
+ "id": doc_id,
274
+ "name": name,
275
+ "description": description,
276
+ "content": content,
277
+ "filepath": None,
278
+ "metadata": metadata,
279
+ }
280
+
281
+ def get_document_metadata(self, doc_id: str) -> dict | None:
282
+ """Get metadata for a specific document.
283
+
284
+ Args:
285
+ doc_id: Document identifier
286
+
287
+ Returns:
288
+ Metadata dictionary or None if not found
289
+
290
+ """
291
+ doc = self.documents.get(doc_id)
292
+ return doc.get("metadata", {}) if doc else None
293
+
294
+ def print_document_info(self, doc_id: str):
295
+ """Print formatted information about a document including metadata.
296
+
297
+ Args:
298
+ doc_id: Document identifier
299
+
300
+ """
301
+ doc = self.documents.get(doc_id)
302
+ if not doc:
303
+ print(f"Document '{doc_id}' not found")
304
+ return
305
+
306
+ print("=" * 70)
307
+ print(f"📚 {doc['name']}")
308
+ print("=" * 70)
309
+ print(f"\nDescription: {doc['description']}")
310
+
311
+ metadata = doc.get("metadata", {})
312
+ if metadata:
313
+ print("\n" + "-" * 70)
314
+ print("METADATA")
315
+ print("-" * 70)
316
+
317
+ # Display key metadata fields
318
+ if "authors" in metadata:
319
+ print(f"Authors: {metadata['authors']}")
320
+ if "affiliations" in metadata:
321
+ print(f"Affiliations: {metadata['affiliations']}")
322
+ if "version" in metadata:
323
+ print(f"Version: {metadata['version']}")
324
+ if "last_updated" in metadata:
325
+ print(f"Last Updated: {metadata['last_updated']}")
326
+ if "license" in metadata:
327
+ print(f"License: {metadata['license']}")
328
+ if "commercial_use" in metadata:
329
+ print(f"Commercial Use: {metadata['commercial_use']}")
330
+ if "status" in metadata:
331
+ print(f"Status: {metadata['status']}")
332
+
333
+ print("=" * 70)
334
+
335
+ def remove_document(self, doc_id: str):
336
+ """Remove a know-how document.
337
+
338
+ Args:
339
+ doc_id: Document identifier
340
+
341
+ """
342
+ if doc_id in self.documents:
343
+ del self.documents[doc_id]
344
+
345
+ def reload(self):
346
+ """Reload all documents from disk."""
347
+ self.documents = {}
348
+ self._load_documents()
Biomanus_upload/biomni/know_how/resource/CRISPick_download_links.txt ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8030_ICSASG_v2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230421.txt.gz
2
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8030_ICSASG_v2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230421.summary.txt
3
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8030_ICSASG_v2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230421.txt.gz
4
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8030_ICSASG_v2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230421.summary.txt
5
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20241114.txt.gz
6
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20241114.summary.txt
7
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20241114.txt.gz
8
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20241114.summary.txt
9
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20241114.txt.gz
10
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20241114.summary.txt
11
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20241114.txt.gz
12
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20241114.summary.txt
13
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241114.txt.gz
14
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10029_CHOK1GS_HDv1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241114.summary.txt
15
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_74940_Otsh_v1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230424.txt.gz
16
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_74940_Otsh_v1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230424.summary.txt
17
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_74940_Otsh_v1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230424.txt.gz
18
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_74940_Otsh_v1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230424.summary.txt
19
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9913_ARS-UCD1.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230418.txt.gz
20
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9913_ARS-UCD1.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230418.summary.txt
21
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9913_ARS-UCD1.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230418.txt.gz
22
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9913_ARS-UCD1.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230418.summary.txt
23
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_CanFam3.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220923.txt.gz
24
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_CanFam3.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220923.summary.txt
25
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_CanFam3.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220923.txt.gz
26
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_CanFam3.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220923.summary.txt
27
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20251031.txt.gz
28
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20251031.summary.txt
29
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20240611.txt.gz
30
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20240611.summary.txt
31
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20251031.txt.gz
32
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20251031.summary.txt
33
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20240611.txt.gz
34
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20240611.summary.txt
35
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20251031.txt.gz
36
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20251031.summary.txt
37
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20240611.txt.gz
38
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20240611.summary.txt
39
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20241031.txt.gz
40
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20241031.summary.txt
41
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20240611.txt.gz
42
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20240611.summary.txt
43
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20241031.txt.gz
44
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20241031.summary.txt
45
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20240611.txt.gz
46
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20240611.summary.txt
47
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20251031.txt.gz
48
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20251031.summary.txt
49
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20240611.txt.gz
50
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20240611.summary.txt
51
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20251031.txt.gz
52
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20251031.summary.txt
53
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20240611.txt.gz
54
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20240611.summary.txt
55
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20251031.txt.gz
56
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20251031.summary.txt
57
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241031.txt.gz
58
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241031.summary.txt
59
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240611.txt.gz
60
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9615_ROS_Cfam_1.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240611.summary.txt
61
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_7227_BDGP6.32_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20230407.txt.gz
62
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_7227_BDGP6.32_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20230407.summary.txt
63
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_7227_BDGP6.32_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20230407.txt.gz
64
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_7227_BDGP6.32_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20230407.summary.txt
65
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_59479_mRhiFer1_v1.p_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230427.txt.gz
66
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_59479_mRhiFer1_v1.p_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230427.summary.txt
67
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_59479_mRhiFer1_v1.p_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230427.txt.gz
68
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_59479_mRhiFer1_v1.p_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230427.summary.txt
69
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20220914.txt.gz
70
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20220914.summary.txt
71
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20220914.txt.gz
72
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20220914.summary.txt
73
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20220914.txt.gz
74
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20220914.summary.txt
75
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20220914.txt.gz
76
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20220914.summary.txt
77
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20220914.txt.gz
78
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20220914.summary.txt
79
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20220914.txt.gz
80
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20220914.summary.txt
81
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220914.txt.gz
82
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220914.summary.txt
83
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220914.txt.gz
84
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_60711_Chlorocebus_sabeus_1.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220914.summary.txt
85
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20241107.txt.gz
86
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20241107.summary.txt
87
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20241107.txt.gz
88
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20241107.summary.txt
89
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20241107.txt.gz
90
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20241107.summary.txt
91
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20241107.txt.gz
92
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20241107.summary.txt
93
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241107.txt.gz
94
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh37_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241107.summary.txt
95
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20250806.txt.gz
96
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20250806.summary.txt
97
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20250718.txt.gz
98
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20250718.summary.txt
99
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20250806.txt.gz
100
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20250806.summary.txt
101
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20250718.txt.gz
102
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20250718.summary.txt
103
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20250806.txt.gz
104
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20250806.summary.txt
105
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20250718.txt.gz
106
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20250718.summary.txt
107
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20250806.txt.gz
108
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20250806.summary.txt
109
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20250718.txt.gz
110
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20250718.summary.txt
111
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20250806.txt.gz
112
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20250806.summary.txt
113
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20250718.txt.gz
114
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20250718.summary.txt
115
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20250806.txt.gz
116
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20250806.summary.txt
117
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20250718.txt.gz
118
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20250718.summary.txt
119
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20250806.txt.gz
120
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20250806.summary.txt
121
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20250718.txt.gz
122
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20250718.summary.txt
123
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20250806.txt.gz
124
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20250806.summary.txt
125
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20250718.txt.gz
126
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20250718.summary.txt
127
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20250806.txt.gz
128
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20250806.summary.txt
129
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20250718.txt.gz
130
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20250718.summary.txt
131
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20250806.txt.gz
132
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20250806.summary.txt
133
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20250718.txt.gz
134
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20250718.summary.txt
135
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20250806.txt.gz
136
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20250806.summary.txt
137
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRi_RS3i-Chen2013_NCBI_20250718.txt.gz
138
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRi_RS3i-Chen2013_NCBI_20250718.summary.txt
139
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241104.txt.gz
140
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241104.summary.txt
141
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241028.txt.gz
142
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241028.summary.txt
143
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20250807.txt.gz
144
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20250807.summary.txt
145
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20250724.txt.gz
146
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20250724.summary.txt
147
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20250807.txt.gz
148
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20250807.summary.txt
149
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20250724.txt.gz
150
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20250724.summary.txt
151
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20250807.txt.gz
152
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20250807.summary.txt
153
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20250724.txt.gz
154
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20250724.summary.txt
155
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20250807.txt.gz
156
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20250807.summary.txt
157
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20250724.txt.gz
158
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20250724.summary.txt
159
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20250807.txt.gz
160
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20250807.summary.txt
161
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20250724.txt.gz
162
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20250724.summary.txt
163
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20250807.txt.gz
164
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20250807.summary.txt
165
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20250724.txt.gz
166
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20250724.summary.txt
167
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20250807.txt.gz
168
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRa_Azimuth_2.0_NCBI_20250807.summary.txt
169
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20250724.txt.gz
170
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRa_Azimuth_2.0_Ensembl_20250724.summary.txt
171
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20250807.txt.gz
172
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20250807.summary.txt
173
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20250724.txt.gz
174
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRi_Azimuth_2.0_Ensembl_20250724.summary.txt
175
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20250807.txt.gz
176
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20250807.summary.txt
177
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20250724.txt.gz
178
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SaurCas9_CRISPRko_Azimuth_2.0_Ensembl_20250724.summary.txt
179
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20250807.txt.gz
180
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20250807.summary.txt
181
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20250724.txt.gz
182
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20250724.summary.txt
183
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRi_RS3i-Chen2013_NCBI_20250807.txt.gz
184
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRi_RS3i-Chen2013_NCBI_20250807.summary.txt
185
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20250724.txt.gz
186
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRi_RS3i-Chen2013_Ensembl_20250724.summary.txt
187
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240614.txt.gz
188
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240614.summary.txt
189
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20230403.txt.gz
190
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10090_GRCm38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20230403.summary.txt
191
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8022_USDA_OmykA_1.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230419.txt.gz
192
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8022_USDA_OmykA_1.1_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20230419.summary.txt
193
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8022_USDA_OmykA_1.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230419.txt.gz
194
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_8022_USDA_OmykA_1.1_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20230419.summary.txt
195
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20241112.txt.gz
196
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRa_Seq-DeepCpf1_Ensembl_20241112.summary.txt
197
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20240131.txt.gz
198
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRa_Seq-DeepCpf1_NCBI_20240131.summary.txt
199
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20241112.txt.gz
200
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRi_Seq-DeepCpf1_Ensembl_20241112.summary.txt
201
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20240131.txt.gz
202
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRi_Seq-DeepCpf1_NCBI_20240131.summary.txt
203
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20241112.txt.gz
204
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRko_Seq-DeepCpf1_Ensembl_20241112.summary.txt
205
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20240131.txt.gz
206
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_AsCas12a_CRISPRko_Seq-DeepCpf1_NCBI_20240131.summary.txt
207
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20241112.txt.gz
208
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRa_enPAM+GB_Ensembl_20241112.summary.txt
209
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20240131.txt.gz
210
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRa_enPAM+GB_NCBI_20240131.summary.txt
211
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20241112.txt.gz
212
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRi_enPAM+GB_Ensembl_20241112.summary.txt
213
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20240131.txt.gz
214
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRi_enPAM+GB_NCBI_20240131.summary.txt
215
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20241112.txt.gz
216
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRko_enPAM+GB_Ensembl_20241112.summary.txt
217
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20240131.txt.gz
218
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_enAsCas12a_CRISPRko_enPAM+GB_NCBI_20240131.summary.txt
219
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20241112.txt.gz
220
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_Ensembl_20241112.summary.txt
221
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20240131.txt.gz
222
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20240131.summary.txt
223
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241112.txt.gz
224
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_Ensembl_20241112.summary.txt
225
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240131.txt.gz
226
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_mRatBN7.2_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20240131.summary.txt
227
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20220907.txt.gz
228
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SaurCas9_CRISPRi_Azimuth_2.0_NCBI_20220907.summary.txt
229
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20220907.txt.gz
230
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SaurCas9_CRISPRko_Azimuth_2.0_NCBI_20220907.summary.txt
231
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220907.txt.gz
232
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20220907.summary.txt
233
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220907.txt.gz
234
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_10116_Rnor_6.0_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20220907.summary.txt
235
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9544_Mmul_10_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20241025.txt.gz
236
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9544_Mmul_10_SpyoCas9_CRISPRa_RS3seq-Chen2013_NCBI_20241025.summary.txt
237
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9544_Mmul_10_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241025.txt.gz
238
+ https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/sgRNA_design_9544_Mmul_10_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241025.summary.txt
Biomanus_upload/biomni/know_how/resource/addgene_grna_sequences.csv ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Application,Cas9 Species,Depositor,Plasmid ID,PubMed ID,Target Gene,Target Sequence,Target Species
2
+ cut,S. pyogenes,Goncalves,"<a href=""/58252/"">58252</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24870050/"">24870050</a>",AAVS1,GGGGCCACTAGGGACAGGAT,H. sapiens
3
+ cut,S. pyogenes,Church,"<a href=""/41818/"">41818</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",AAVS1,GGGGCCACTAGGGACAGGAT,H. sapiens
4
+ cut,S. pyogenes,Sabatini,"<a href=""/50662/"">50662</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336569/"">24336569</a>",AAVS1,GGGGCCACTAGGGACAGGAT,H. sapiens
5
+ cut,S. pyogenes,Sabatini,"<a href=""/70661/"">70661</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",AAVS1,GGGGCCACTAGGGACAGGAT,H. sapiens
6
+ cut,S. pyogenes,Church,"<a href=""/41817/"">41817</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",AAVS1,GTCCCCTCCACCCCACAGTG,H. sapiens
7
+ cut,S. pyogenes,Kanemaki,"<a href=""/72833/"">72833</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27052166/"">27052166</a>",AAVS1,GGGGCCACTAGGGACAGGAT,H. sapiens
8
+ cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",AAVS1,TGTCCCTAGTGGCCCCACTG,H. sapiens
9
+ cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",AAVS1,ACAGTGGGGCCACTAGGGAC,H. sapiens
10
+ RNA targeting,S. pyogenes,Yeo,"<a href=""/74705/"">74705</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26997482/"">26997482</a>",ACTB,UGGAGCGAGCAUCCCCCAAA,H. sapiens
11
+ cut,S. pyogenes,Jiang,"<a href=""/62552/"">62552</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25739462/"">25739462</a>",actII-orf4,ATTACCAGGGACCGGAGTTC,S. coelicolor
12
+ cut,S. pyogenes,Zaratiegui,"<a href=""/52225/"">52225</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25352017/"">25352017</a>",ade6+,TCTATTGTTCAGATGCCTCG,S. pombe
13
+ cut,S. pyogenes,Zaratiegui,"<a href=""/52227/"">52227</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25352017/"">25352017</a>",ade6-L469,TCTATTGTTCAGATGCCTTG,S. pombe
14
+ cut,S. pyogenes,Zaratiegui,"<a href=""/52226/"">52226</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25352017/"">25352017</a>",ade6-M210,TCTATTGTTCAGATGCTTCG,S. pombe
15
+ cut,S. pyogenes,Ventura,"<a href=""/64071/"">64071</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25337876/"">25337876</a>",Alk and Eml,,M. musculus
16
+ nick,S. pyogenes,Shaw,"<a href=""/74374/"">74374</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26816379/"">26816379</a>",AMPK alpha 1,GGCTGTCGCCATCTTTCTCC,H. sapiens
17
+ nick,S. pyogenes,Shaw,"<a href=""/74375/"">74375</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26816379/"">26816379</a>",AMPK alpha 1,GAAGATCGGCCACTACATTC,H. sapiens
18
+ nick,S. pyogenes,Shaw,"<a href=""/74376/"">74376</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26816379/"">26816379</a>",AMPK alpha 2,GTCAGCCATCTTCGGCGCGCG,H. sapiens
19
+ nick,S. pyogenes,Shaw,"<a href=""/74377/"">74377</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26816379/"">26816379</a>",AMPK alpha 2,GAAGATCGGACACTACGTGC,H. sapiens
20
+ cut,S. pyogenes,Sabatini,"<a href=""/70660/"">70660</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>","Amplicon, JAK2",GAGGCATATTCTTCTCCTGG,H. sapiens
21
+ cut,S. pyogenes,Voytas,"<a href=""/70019/"">70019</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26541286/"">26541286</a>",ANT1,TGTCGTTTATAATTTGTAGA,S. lycopersicum
22
+ cut,S. pyogenes,Voytas,"<a href=""/70018/"">70018</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26541286/"">26541286</a>",ANT1,ACAATTTAATACACCTTTT,S. lycopersicum
23
+ cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",AOC,CAAAAGACTGTCAATTCCCT,N. attenuata
24
+ cut/nick,S. pyogenes,Yamamoto,"<a href=""/58780/"">58780</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",APC,GAAGCGGGCAAAGGGGCGAC,H. sapiens
25
+ cut,S. pyogenes,Joung,"<a href=""/42241/"">42241</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",apoea,GGATGAGCCAAGAAGCCGCT,D. rerio
26
+ activate,S. pyogenes,Sato,"<a href=""/64131/"">64131</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",ASCL1,TGGATGGAGAGTTTGCAAGGAGC,H. sapiens
27
+ activate,S. pyogenes,Sato,"<a href=""/64130/"">64130</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",ASCL1,TGGGCAGCCGCTCGCTGCAGCAG,H. sapiens
28
+ activate,S. pyogenes,Sato,"<a href=""/64129/"">64129</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",ASCL1,TGGGGCTGGGTGTCCCATTGAAA,H. sapiens
29
+ activate,S. pyogenes,Sato,"<a href=""/64132/"">64132</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",ASCL1,TGGTGTTTATTCAGCCGGGAGTC,H. sapiens
30
+ cut,S. pyogenes,Mashimo,"<a href=""/60968/"">60968</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Asip,ATGACAGGAGTCTAACTCAC,R. norvegicus
31
+ tag,S. pyogenes,Mendenhall,"<a href=""/64690/"">64690</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",ATF1,TAGGAATCAAACACTTTTATTGG,H. sapiens
32
+ cut,S. pyogenes,Yamamoto,"<a href=""/58779/"">58779</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",ATM,TGAATTGGGATGCTGTTTTT,H. sapiens
33
+ cut,S. pyogenes,Mello,"<a href=""/58981/"">58981</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24879462/"">24879462</a>",avr-14,GATTGGAGAGTTAGACCACG,C. elegans
34
+ cut,S. pyogenes,Mello,"<a href=""/58982/"">58982</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24879462/"">24879462</a>",avr-15,GTTTGCAATATAAGTCACCC,C. elegans
35
+ cut,S. pyogenes,Yamamoto,"<a href=""/58782/"">58782</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",AXIN2,TATGTTGGTGACTTGCCTCC,H. sapiens
36
+ methylation,S. pyogenes,Zoldoš,"<a href=""/71828/"">71828</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26969735/"">26969735</a>",BACH2,AATGTAGCGATTGAGAGTGTGGG,H. sapiens
37
+ cut,S. pyogenes,Sabatini,"<a href=""/70658/"">70658</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",BCR/ABL amplicon,GGCTCCCTTCAAGTGGGATG,H. sapiens
38
+ cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",BFP,ATGGCGTGCAGTGCTTCAGC,
39
+ cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",BIN2,ATCACAGTGATGCTCGTCAA,L. sativa
40
+ cut,S. pyogenes,Hanna,"<a href=""/59724/"">59724</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25543152/"">25543152</a>",BLIMP1,CGGATGGGGTAAACGACCCG,H. sapiens
41
+ cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",BRI1,TTTGAAAGATGGAAGCGCGG,A. thaliana
42
+ cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",BRI1,TGAAACTAAACTGGTCCACA,A. thaliana
43
+ interfere,S. pyogenes,Lu,"<a href=""/68892/"">68892</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",BT1754,GAAAATGGGGTGTATCCTGC,B. thetaiotaomicron
44
+ interfere,S. pyogenes,Lu,"<a href=""/68891/"">68891</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",BT1854,ATTGAAGAACAAAAGCAGTT,B. thetaiotaomicron
45
+ cut,S. pyogenes,Sabatini,"<a href=""/70650/"">70650</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C16orf80,CGATGCTGTAGAGGATGGAG,H. sapiens
46
+ cut,S. pyogenes,Sabatini,"<a href=""/70651/"">70651</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C16orf80,TGTCTGAGAAGTAAACCCGT,H. sapiens
47
+ cut,S. pyogenes,Sabatini,"<a href=""/70653/"">70653</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C3orf17,GGGCCAAATGGGGTTTGTGG,H. sapiens
48
+ cut,S. pyogenes,Sabatini,"<a href=""/70652/"">70652</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C3orf17,GTGTGAGAATCCCTAAGGCG,H. sapiens
49
+ cut,S. pyogenes,Sabatini,"<a href=""/70654/"">70654</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C9orf114,CAGGCGGGCTCACCTCCGTG,H. sapiens
50
+ cut,S. pyogenes,Sabatini,"<a href=""/70655/"">70655</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",C9orf114,GCGGCAGAGAAGGAGGACCG,H. sapiens
51
+ cut,S. pyogenes,Church,"<a href=""/43803/"">43803</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23460208/"">23460208</a>",CAN1,GATACGTTCTCTATGGAGGA,S. cerevisiae
52
+ cut,S. pyogenes,Bornhauser,"<a href=""/75164/"">75164</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27194728/"">27194728</a>",Caspase-8,GCCTGGACTACATTCCGCAA,H. sapiens
53
+ cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",CCR5,TGACATCAATTATTATACAT,H. sapiens
54
+ interfere,S. pyogenes,Qi,"<a href=""/46918/"">46918</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",CD71,GGACGCGCTAGTGTGAGTGC,H. sapiens
55
+ cut,S. pyogenes,Yamamoto,"<a href=""/58781/"">58781</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",CDH1,TGACTTGCGAGGGACGCATT,H. sapiens
56
+ activate,S. pyogenes,Vale,"<a href=""/60905/"">60905</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25307933/"">25307933</a>",CDKN1B,GAAGCCGGGACCTGGACCAG,H. sapiens
57
+ tag,S. pyogenes,Mendenhall,"<a href=""/64036/"">64036</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",CEBPB,CTCCGGCCACTGCTAGCGCGG,H. sapiens
58
+ tag,S. pyogenes,Mendenhall,"<a href=""/64047/"">64047</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",CEBPB,GCCGGCAGGGGGACGCGCGCGG,H. sapiens
59
+ cut,S. pyogenes,Ikawa,"<a href=""/50718/"">50718</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24284873/"">24284873</a>",Cent1,GGCAACGTTTGACTTCCTGA,M. musculus
60
+ cut,S. pyogenes,Yamamoto,"<a href=""/58783/"">58783</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",CFTR,TCTGTATCTATATTCATCAT,H. sapiens
61
+ tag,S. pyogenes,Mendenhall,"<a href=""/64940/"">64940</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",CREB1,GCCACAAATCAGATTAATTTGG,H. sapiens
62
+ tag,S. pyogenes,Mendenhall,"<a href=""/64939/"">64939</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",CREB1,GCCACAAATCAGATTAATTTGGG,H. sapiens
63
+ cut,S. pyogenes,Hong,"<a href=""/68060/"">68060</a>","<a href=""http://fungalbiolbiotech.biomedcentral.com/articles/10.1186/s40694-015-0015-1"">Fungal Biology and Biotechnology 2015, 2:4</a>",csr-1,GAGTGGGAGGGTCCCGTCCT,N. crassa
64
+ cut,S. pyogenes,Jacks,"<a href=""/59912/"">59912</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25119044/"">25119044</a>",Ctnnb1,AGCTCCTTCCCTGAGTGGCA,M. musculus
65
+ cut,S. pyogenes,Jacks,"<a href=""/59911/"">59911</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25119044/"">25119044</a>",Ctnnb1,CTGTGGTGGTGGCACCAGAA,M. musculus
66
+ interfere,S. pyogenes,Qi,"<a href=""/46917/"">46917</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",CXCR4,GCAGGTAGCAAAGTGACGCCGA,H. sapiens
67
+ cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",CXCR4,CCTCTTTGTCATCACGCTTC,H. sapiens
68
+ activate,S. pyogenes,Lu,"<a href=""/64385/"">64385</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,ACAGAGCACATGCATGCCAT,S. cerevisiae
69
+ activate,S. pyogenes,Lu,"<a href=""/64387/"">64387</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,ACTAATACTTTCAACATTTT,S. cerevisiae
70
+ activate,S. pyogenes,Lu,"<a href=""/64382/"">64382</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,ATATCGAATTCCTGCAGCCC,S. cerevisiae
71
+ activate,S. pyogenes,Lu,"<a href=""/64380/"">64380</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,ATATTCTTTCCTTATACATT,S. cerevisiae
72
+ activate,S. pyogenes,Lu,"<a href=""/64379/"">64379</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,CTAGATATTAAAATGTCTAA,S. cerevisiae
73
+ activate,S. pyogenes,Lu,"<a href=""/64388/"">64388</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,GTTGAAAGTATTAGTTAAAG,S. cerevisiae
74
+ activate,S. pyogenes,Lu,"<a href=""/64381/"">64381</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,TACATACAGTAGGATCCTA,S. cerevisiae
75
+ activate,S. pyogenes,Lu,"<a href=""/64383/"">64383</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,TATAGTAATTTATGCTGCAA,S. cerevisiae
76
+ activate,S. pyogenes,Lu,"<a href=""/64384/"">64384</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,TGCAAAGGTCCTAATGTATA,S. cerevisiae
77
+ activate,S. pyogenes,Lu,"<a href=""/64386/"">64386</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,TGTATGTACATACAGTACCC,S. cerevisiae
78
+ activate,S. pyogenes,Lu,"<a href=""/64378/"">64378</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23977949/"">23977949</a>",CYC1m promoter,TTACTGTATGTACATACAGT,S. cerevisiae
79
+ cut,S. pyogenes,Wente,"<a href=""/47932/"">47932</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23918387/"">23918387</a>",ddx19,GGCAACAGATTCGTGGGCCC,D. rerio
80
+ cut,S. pyogenes,Sabatini,"<a href=""/70656/"">70656</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",DDX3Y,GCAGTTTAGCGATATTGACA,H. sapiens
81
+ cut,S. pyogenes,Sabatini,"<a href=""/70657/"">70657</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",DDX3Y,TCTTGTTGGGGCTAAAACCA,H. sapiens
82
+ cut,S. pyogenes,Church,"<a href=""/41822/"">41822</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",DNMT3A,GAGATGATCGCCCCTTCTTC,H. sapiens
83
+ cut,S. pyogenes,Church,"<a href=""/41821/"">41821</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",DNMT3A,GCATGATGCGCGGCCCAAGG,H. sapiens
84
+ cut,S. pyogenes,Church,"<a href=""/41823/"">41823</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",DNMT3B,GAATTACTCACGCCCCAAGG,H. sapiens
85
+ cut,VRER Cas9 mutant,Fire,"<a href=""/71516/"">71516</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26680661/"">26680661</a>",dpy-10,CGCTACCATAGGCACCACG,C. elegans
86
+ cut,S. pyogenes,Katic,"<a href=""/65630/"">65630</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26044730/"">26044730</a>",dpy-10,GCTACCATAGGCACCACGAG,C.elegans
87
+ cut,S. pyogenes,Fire,"<a href=""/59933/"">59933</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",dpy-10,GCTACCATAGGCACCACGAG,C. elegans
88
+ cut,VQR Cas9 variant,Fire,"<a href=""/71479/"">71479</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26680661/"">26680661</a>",dpy-10,TCCGCTACCATAGGCACCA,C. elegans
89
+ cut,S. pyogenes,Seydoux,"<a href=""/70047/"">70047</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26187122/"">26187122</a>",dpy‐10,GCTACCATAGGCACCACGAG,C. elegans
90
+ cut,S. pyogenes,Joung,"<a href=""/42242/"">42242</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",drd3,GGAAACTACAGCCCAGCGTC,D. rerio
91
+ cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",DWD1,TGCATCGTCCAAGCGCACAG,O. sativa
92
+ cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",DWD1,CTACGACGTCAGGTTCTACC,O. sativa
93
+ cut,S. pyogenes,Christiaen,"<a href=""/59990/"">59990</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25336740/"">25336740</a>",Ebf,GCTGAGGGTTGGACAACAGG,C. intestinalis
94
+ dCas9-FokI,S. pyogenes,Joung,"<a href=""/60071/"">60071</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24770325/"">24770325</a>",EGFP,"multiple, see article",A. victoria
95
+ cut,S. pyogenes,Zhang,"<a href=""/51764/"">51764</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GAAGTTCGAGGGCGACACCC,A. victoria
96
+ cut,S. pyogenes,Zhang,"<a href=""/51761/"">51761</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GAGCTGGACGGCGACGTAAA,A. victoria
97
+ cut,S. pyogenes,Joung,"<a href=""/47512/"">47512</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",EGFP,GATGCCGTTCTTCTGCTTGT,A. victoria
98
+ cut,S. pyogenes,Zhang,"<a href=""/51763/"">51763</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GGAGCGCACCATCTTCTTCA,A. victoria
99
+ cut,S. pyogenes,Zhang,"<a href=""/51762/"">51762</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GGCCACAAGTTCAGCGTGTC,A. victoria
100
+ cut,S. pyogenes,Del Bene,"<a href=""/61051/"">61051</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24179142/"">24179142</a>",EGFP,GGCGAGGGCGATGCCACCTA,A. victoria
101
+ cut,S. pyogenes,Chen,"<a href=""/46760/"">46760</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23918387/"">23918387</a>",EGFP,GGGCACGGGCAGCTTGCCGG,A. victoria
102
+ cut,S. pyogenes,Joung,"<a href=""/47511/"">47511</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",EGFP,GGGCACGGGCAGCTTGCCGG,A. victoria
103
+ cut,S. pyogenes,Zhang,"<a href=""/51760/"">51760</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GGGCGAGGAGCTGTTCACCG,A. victoria
104
+ cut,S. pyogenes,Zhang,"<a href=""/51765/"">51765</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24336571/"">24336571</a>",EGFP,GGTGAACCGCATCGAGCTGA,A. victoria
105
+ cut,S. pyogenes,Joung,"<a href=""/47513/"">47513</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",EGFP,GGTGGTGCAGATGAACTTCA,A. victoria
106
+ cut,S. pyogenes,Goncalves,"<a href=""/58255/"">58255</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24870050/"">24870050</a>",EGFP,TGAAGAAGATGGTGCGCTC,A. victoria
107
+ cut,S. pyogenes,Moffat,"<a href=""/74187/"">74187</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",EIF3D,TGTAGGTTGCCTCCATGGCC,H. sapiens
108
+ cut,S. pyogenes,Moffat,"<a href=""/74188/"">74188</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",EIF3D,AGACGACCCTGTCATCCGCA,H. sapiens
109
+ cut,S. pyogenes,Zhang,"<a href=""/42337/"">42337</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287718/"">23287718</a>",Emx1,,H. sapiens
110
+ cut,S. pyogenes,Joung,"<a href=""/47508/"">47508</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",EMX1,GAGTCCGAGCAGAAGAAGAA,H. sapiens
111
+ cut,S. pyogenes,Corn,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26789497/"">26789497</a>",EMX1,CGATGTCACCTCCAATGACT,H. sapiens
112
+ cut,S. pyogenes,Chen,"<a href=""/71288/"">71288</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26193878/"">26193878</a>","ETC2, TRY, and CPC",,A. thaliana
113
+ cut,S. pyogenes,Bornhauser,"<a href=""/75166/"">75166</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27194728/"">27194728</a>",FADD,ACTCCCGCACACGCTCTGTC,H. sapiens
114
+ cut,S. pyogenes,Joung,"<a href=""/47510/"">47510</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",FANCF,GGAATCCCTTCTGCAGCACC,H. sapiens
115
+ cut,S. pyogenes,Seydoux,"<a href=""/65597/"">65597</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",fbf-2,GTAGTCACGGCGATGATTA,C. elegans
116
+ cut,S. pyogenes,Seydoux,"<a href=""/65591/"">65591</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",fbf-2,TAATCATCGCCGTGACTAC,C. elegans
117
+ cut,S. pyogenes,Joung,"<a href=""/42244/"">42244</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",fh,GGAGCGAGCGGAGCGGTACA,D. rerio
118
+ cut,S. pyogenes,Joung,"<a href=""/42243/"">42243</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",fh,GGAGCGGTACATGGCGACCG,D. rerio
119
+ tag,S. pyogenes,Mendenhall,"<a href=""/64254/"">64254</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",GABPA,GAAAAGGATAATTGAGCCCCAGG,H. sapiens
120
+ tag,S. pyogenes,Mendenhall,"<a href=""/64255/"">64255</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",GABPA,TTTGGAGTCTCAGAATGTCCTGG,H. sapiens
121
+ activate,S. pyogenes,Qi,"<a href=""/46916/"">46916</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",GAL4 UAS,GAACGACTAGTTAGGCGTGTA,
122
+ activate,S. pyogenes,Qi,"<a href=""/46915/"">46915</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",GAL4 UAS,GTTGGAGCACTGTCCTCCGAACGT,
123
+ activate,S. pyogenes,Sato,"<a href=""/64158/"">64158</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",GAL4UAS,TGGGGACAGTACTCCGCTCGAGT,
124
+ activate,S. pyogenes,Sato,"<a href=""/64157/"">64157</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",GAL4UAS,TGGGTCTTCGGAGGACAGTACTC,
125
+ activate,S. pyogenes,Sato,"<a href=""/64159/"">64159</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",GAL4UAS,TGGTCCGTCTAGAAACTCGGTAC,
126
+ cut,S. pyogenes,Du,"<a href=""/65566/"">65566</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25849248/"">25849248</a>",gfap,GTGCGCAACACATAGCACCA,D. rerio
127
+ interfere,S. pyogenes,Qi,"<a href=""/46914/"">46914</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",GFP,GAATAGCTCAGAGGCCGAGG,A. victoria
128
+ cut,S. pyogenes,Church,"<a href=""/41820/"">41820</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",GFP,GGAGCGCACCATCTTCTTCA,A. victoria
129
+ cut,S. pyogenes,Church,"<a href=""/41819/"">41819</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23287722/"">23287722</a>",GFP,GTGAACCGCATCGAGCTGAA,A. victoria
130
+ cut,S. pyogenes,"Guigo, Johnson","<a href=""/72619/"">72619</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",GFP,gRNA1: GAGCTGGACGGCGACGTAAA; gRNA2: CAGAACACCCCCATCGGCGA,Synthetic
131
+ interfere,S. pyogenes,Hudson,"<a href=""/73221/"">73221</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26689101/"">26689101</a>",GFPmut3b,ACCATCTAATTCAACAAGAATT,A. victoria
132
+ interfere,S. pyogenes,Hudson,"<a href=""/73224/"">73224</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26689101/"">26689101</a>",GFPmut3b,ACCATCTAATTCAACAAGAATT,synthetic
133
+ CRISPR-display,S. pyogenes,Rinn,"<a href=""/68422/"">68422</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26030444/"">26030444</a>",GLuc,GATCTAGATACGACTCACTAT,synthetic
134
+ cut,S. pyogenes,Ye,"<a href=""/61833/"">61833</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24424410/"">24424410</a>",gp78,GCCCAGCCTCCGCACCTACA,H. sapiens
135
+ cut,S. pyogenes,Joung,"<a href=""/42245/"">42245</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",gsk3b,GGGACCTGACCGGCCGCAGG,D. rerio
136
+ cut,S. pyogenes,Jin,"<a href=""/64333/"">64333</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25281382/"">25281382</a>",his3,ATTGCGATCTCTTTAAAGGG,S. cerevisiae
137
+ cut,S. pyogenes,Yan,"<a href=""/69539/"">69539</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26130722/"">26130722</a>",HPRT,GACTGTAAGTGAATTACTT,H. sapiens
138
+ cut,S. pyogenes,Yamamoto,"<a href=""/58778/"">58778</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",HPRT1,CTGATAAAATCTACAGTCAT,H. sapiens
139
+ cut,S. pyogenes,Yamamoto,"<a href=""/58771/"">58771</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24954249/"">24954249</a>",HPRT1,TTATGCTGAGGATTTGGAAA,H. sapiens
140
+ activate,S. pyogenes,Sato,"<a href=""/64142/"">64142</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",IL1RN,TGGACGCAGATAAGAACCAGTT,H. sapiens
141
+ activate,S. pyogenes,Sato,"<a href=""/64151/"">64151</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",IL1RN,TGGCATCAAGTCAGCCATCAGC,H. sapiens
142
+ activate,S. pyogenes,Sato,"<a href=""/64152/"">64152</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",IL1RN,TGGGAGTCACCCTCCTGGAAAC,H. sapiens
143
+ activate,S. pyogenes,Sato,"<a href=""/64140/"">64140</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",IL1RN,TGGTGTACTCTCTGAGGTGCTC,H. sapiens
144
+ cut,S. pyogenes,Xue,"<a href=""/66582/"">66582</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",inverted GFP,GAGCGGCCGCTCGAGTCTAG,A. victoria
145
+ cut,S. pyogenes,Xue,"<a href=""/66583/"">66583</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",inverted GFP,GATATCGAATTCCTGCAGCC,A. victoria
146
+ cut,S. pyogenes,Xue,"<a href=""/66581/"">66581</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",inverted GFP,GTATCGATACCGTCGACCTCG,A. victoria
147
+ cut,S. pyogenes,Xue,"<a href=""/66584/"">66584</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",inverted GFP,GTTGATCCATAACTTCGTAT,A. victoria
148
+ purify,S. pyogenes,Fujii,"<a href=""/61079/"">61079</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23942116/"">23942116</a>",IRF1,CCGGGGGCGCTGGGCTGTCCCGG,H. sapiens
149
+ purify,S. pyogenes,Fujii,"<a href=""/61080/"">61080</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25051498/"">25051498</a>",IRF1,GCGCTTACACTTTAGGAGACACTC,H. sapiens
150
+ cut,S. pyogenes,Sabatini,"<a href=""/70679/"">70679</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26472758/"">26472758</a>",JAK2 amplicon&nbsp;,GGTTTAATGGAAGAGAAGGG,H. sapiens
151
+ cut,S. pyogenes,Seydoux,"<a href=""/66085/"">66085</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",K08F4.2,AATCACTCCCTGTTTGTGT,C. elegans
152
+ cut,S. pyogenes,Seydoux,"<a href=""/66095/"">66095</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",K08F4.2,CACGAGGTGGTATGCGCAG,C. elegans
153
+ cut,S. pyogenes,Seydoux,"<a href=""/66092/"">66092</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",K08F4.2,CGCAGCGGTTTCCAAAATG,C. elegans
154
+ cut,S. pyogenes,Seydoux,"<a href=""/66087/"">66087</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",K08F4.2,GCCTTAACCCAGAATAAGA,C. elegans
155
+ cut,S. pyogenes,Seydoux,"<a href=""/66089/"">66089</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",K08F4.2,TATTAAATGCAGATAACCT,C. elegans
156
+ cut,S. pyogenes,Wolfe,"<a href=""/69238/"">69238</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",KANK3,GCATGGGTGATGTCAATGCC,H. sapiens
157
+ cut,S. pyogenes,Mashimo,"<a href=""/60969/"">60969</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Kit-1,CATCTGTGCGGCCGTTGGCT,R. norvegicus
158
+ cut,S. pyogenes,Mashimo,"<a href=""/60970/"">60970</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Kit-2,CTAACGTTCCAGCGCTCGTT,R. norvegicus
159
+ cut,S. pyogenes,Mashimo,"<a href=""/60971/"">60971</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Kit-2,GTCAAGATGTCATCTTACGG,R. norvegicus
160
+ cut,S. pyogenes,Calarco,"<a href=""/46170/"">46170</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23817069/"">23817069</a>",klp-12,GATCCACAAGTTACAATTGG,C. elegans
161
+ cut,S. pyogenes,Zhang,"<a href=""/60224/"">60224</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25263330/"">25263330</a>","Kras, p53, and Lkb1","multiple, see article",M. musculus
162
+ cut,S. pyogenes,Zhang,"<a href=""/60225/"">60225</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25263330/"">25263330</a>",LacZ,TGCGAATACGCCCACGCGAT,E. coli
163
+ cut,S. pyogenes,Moffat,"<a href=""/74179/"">74179</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",LacZ,CCCGAATCTCTATCGTGCGG,E.coli
164
+ cut,S. pyogenes,Patron,"<a href=""/68256/"">68256</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26616834/"">26616834</a>",lC.GA4a,GTTGAGAGGGGAGCCGGTGA,B. oleracea
165
+ cut,S. pyogenes,Patron,"<a href=""/68255/"">68255</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26616834/"">26616834</a>",lC.GA4a,GTTTTCACTTGCGGCCGGAG,B. oleracea
166
+ cut,S. pyogenes,Jin,"<a href=""/64332/"">64332</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25281382/"">25281382</a>",leu2,AATCACAGCCGAAGCCATTA,S. cerevisiae
167
+ cut,S. pyogenes,Winslow,"<a href=""/66894/"">66894</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26178787/"">26178787</a>",Lkb1,GTGGTGGGCCGCAGTCACAA,M. musculus
168
+ cut,S. pyogenes,Xue,"<a href=""/66586/"">66586</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",Lox-STOP-Lox,GCGTATAGCATACATTATACG,synthetic
169
+ cut,S. pyogenes,Xue,"<a href=""/66585/"">66585</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",Lox-STOP-Lox,GCGTATAGTACACATTATACG,synthetic
170
+ cut,S. pyogenes,Wolfe,"<a href=""/69237/"">69237</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",LRTM2,GCTGGCGGAAGACAGAGTGC,H. sapiens
171
+ cut,S. pyogenes,Moffat,"<a href=""/74190/"">74190</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",Luciferase,ACAACTTTACCGACCGCGCC,
172
+ cut,S. pyogenes,Cate,"<a href=""/60847/"">60847</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25139909/"">25139909</a>",LYP1,CATAATAACGTCCAATAAAT,S. cerevisiae
173
+ cut,S. pyogenes,"Guigo, Johnson","<a href=""/72620/"">72620</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",Malat1,gRNA1: GAACCGGTGGGGCTGCGTCA; gRNA2: GGCAGGAGAGGCCAGTTGCG,H. sapiens
174
+ cut,S. pyogenes,"Guigo, Johnson","<a href=""/72621/"">72621</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",Malat1,gRNA1: GCAACTTCCATTTTCAGTCT; gRNA2: GGAAGCCTCAGCTCGCCTGA,H. sapiens
175
+ cut,S. pyogenes,"Guigo, Johnson","<a href=""/72622/"">72622</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",Malat1,gRNA1:GCTGGGGCTCAGTTGCGTAA; gRNA2:AGGTTTCTAAAACATGACGG,H. sapiens
176
+ cut,S. pyogenes,"Guigo, Johnson","<a href=""/72623/"">72623</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",Malat1,gRNA1:GTTGAGATGAAGCTTCTTCA; gRNA2:TCAACCGTCCCTGCAAGGCT,H. sapiens
177
+ cut,S. pyogenes,"Guigo, Johnson","<a href=""/72624/"">72624</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",Malat1,gRNA1: GAAACCTCGTGTAGCTATCA; gRNA2: AATGTGAAGGACTTTCGTAA,H. sapiens
178
+ nick,S. pyogenes,Hanna,"<a href=""/61514/"">61514</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25569111/"">25569111</a>",Mettl14,GCCGCTCCCGGATCTCCTGC,M. musculus
179
+ nick,S. pyogenes,Hanna,"<a href=""/61515/"">61515</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25569111/"">25569111</a>",Mettl14,GCGGCAGCTCCTAGCTCAGC,M. musculus
180
+ cut,S. pyogenes,Zhao,"<a href=""/69801/"">69801</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26293209/"">26293209</a>",mitf,CTTTCGGATATAATCCACGG,S. scrofa&nbsp;
181
+ cut,S. pyogenes,Chen,"<a href=""/47931/"">47931</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23918387/"">23918387</a>",mitfa,GGTCTCTCGCAGGATGTTGC,D. rerio
182
+ cut,S. pyogenes,Bleris,"<a href=""/62715/"">62715</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25527740/"">25527740</a>",mKate,ATGAGAATCAAGGCGGTCGA,synthetic
183
+ cut,S. pyogenes,Bornhauser,"<a href=""/75167/"">75167</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27194728/"">27194728</a>",MLKL,ATCCCCGTGGATTCTGCTAA,H. sapiens
184
+ cut,S. pyogenes,Katsanis,"<a href=""/72890/"">72890</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26429889/"">26429889</a>",mmp21,CGGAGCTGATCACTGACA,D. rerio
185
+ visualize,S. pyogenes,Qi,"<a href=""/51025/"">51025</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24360272/"">24360272</a>",MUC4,GTGGCGTGACCTGTGGATGCTG,H. sapiens
186
+ activate,S. pyogenes,Sato,"<a href=""/64138/"">64138</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",MYOD1,TGGCCTCCCTCCCTGCCCGGTAG,H. sapiens
187
+ activate,S. pyogenes,Sato,"<a href=""/64139/"">64139</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",MYOD1,TGGGAGGTTTGGAAAGGGCGTGC,H. sapiens
188
+ activate,S. pyogenes,Sato,"<a href=""/64136/"">64136</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",MYOD1,TGGGCCTGGGCTCCGGGGCGTTT,H. sapiens
189
+ activate,S. pyogenes,Sato,"<a href=""/64137/"">64137</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",MYOD1,TGGGGGCCCCTGCGGCCACCCCG,H. sapiens
190
+ activate,S. pyogenes,Sato,"<a href=""/64156/"">64156</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",NANOG,TGGCATATTCCTGATTTAAAAGT,H. sapiens
191
+ activate,S. pyogenes,Sato,"<a href=""/64153/"">64153</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",NANOG,TGGCGCCAGGAGGGGTGGGTCTA,H. sapiens
192
+ activate,S. pyogenes,Sato,"<a href=""/64154/"">64154</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",NANOG,TGGGCCTTGGTGAGACTGGTAGA,H. sapiens
193
+ activate,S. pyogenes,Sato,"<a href=""/64155/"">64155</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",NANOG,TGGTGTCTTCAGGTTCTGTTGCT,H. sapiens
194
+ interfere,S. pyogenes,Lu,"<a href=""/68897/"">68897</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",NanoLuc,AGCTTACGCCACCCTGTTCC,synthetic
195
+ interfere,S. pyogenes,Lu,"<a href=""/68898/"">68898</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",NanoLuc,GACAGAACGATGCGCTGAAT,synthetic
196
+ interfere,S. pyogenes,Lu,"<a href=""/68895/"">68895</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",NanoLuc,TCACGCTCACACCCAGGTTC,synthetic
197
+ interfere,S. pyogenes,Lu,"<a href=""/68896/"">68896</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",NanoLuc,TTGATCCAAATTATAACCCG,synthetic
198
+ cut,S. pyogenes,Lu,"<a href=""/61270/"">61270</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25240928/"">25240928</a>",NDM-1,GGGCAGTCGCTTCCAACGGTTTGATCGTCA,
199
+ cut,S. pyogenes,Cepko,"<a href=""/69992/"">69992</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25155555/"">25155555</a>",near LoxP sites,CGAAGTTATATTAAGGGTTC,synthetic
200
+ interfere,S. pyogenes,Lu,"<a href=""/68893/"">68893</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",negative control,CTGGAATGAATTGGCCTATG,
201
+ cut,S. pyogenes,Winslow,"<a href=""/66895/"">66895</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26178787/"">26178787</a>",negative control,GCGAGGTATTCGGCTCCGCG,M. musculus
202
+ cut,S. pyogenes,Christiaen,"<a href=""/60006/"">60006</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25336740/"">25336740</a>",negative control,GCTTTGCTACGATCTACATT,C. intestinalis
203
+ cut,S. pyogenes,Bleris,"<a href=""/64955/"">64955</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25527740/"">25527740</a>",negative control,GTCAAGGCACTCTTGCCTA,synthetic
204
+ ,S. pyogenes,Wolfe,"<a href=""/50927/"">50927</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",negative control,GTTCCGCGTTACATAACTTA,H. sapiens
205
+ cut,S. pyogenes,Winslow,"<a href=""/67594/"">67594</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26178787/"">26178787</a>",Neomycin,TCATGGCTGATGCAATGCGG,
206
+ cut,S. pyogenes,Zhang,"<a href=""/60227/"">60227</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25263330/"">25263330</a>",NeuN,TCCGGTTCAGGGACCCCGAC,M. musculus
207
+ activate,S. pyogenes,Sato,"<a href=""/64161/"">64161</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",Neurog2,GTCTCTATCACTGATAGGGA,H. sapiens
208
+ activate,S. pyogenes,Sato,"<a href=""/64160/"">64160</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25619936/"">25619936</a>",Neurog2,GTGAATGATGATAATACGAT,H. sapiens
209
+ methylation,S. pyogenes,Zoldoš,"<a href=""/71830/"">71830</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26969735/"">26969735</a>",Non-targeting,GTAGGCGCGCCGCTCTCTAC,H. sapiens
210
+ cut,S. pyogenes,Huangfu,"<a href=""/66989/"">66989</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26028531/"">26028531</a>",OCT4,CTCCCATGCATTCAAACTG,H. sapiens
211
+ activate,S. pyogenes,Otonkoski,"<a href=""/69537/"">69537</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26352799/"">26352799</a>",OCT4,"multiple, see article",H. sapiens
212
+ cut,N. meningitidis,Thomson,"<a href=""/47870/"">47870</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23940360/"">23940360</a>",OCT4,GTTGTAGCTCCCTTTCTCATTTCG,H. sapiens
213
+ interfere,S. pyogenes,Wolfe,"<a href=""/50922/"">50922</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Oct4A (POU5F1),GGGGCGCCAGTTGTGTCTCC,H. sapiens
214
+ interfere,S. pyogenes,Wolfe,"<a href=""/50921/"">50921</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Oct4A (POU5F1),GTGGGACTGGGGAGGGAGAG,H. sapiens
215
+ cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",P450,CATATAGTTGGGTCATGGCA,O. sativa
216
+ cut,S. pyogenes,Sherwood,"<a href=""/71483/"">71483</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26527385/"">26527385</a>",Pal1,GCTCTGTGACTAGTCACAGAG,synthetic
217
+ cut,S. pyogenes,Sherwood,"<a href=""/71484/"">71484</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26527385/"">26527385</a>",Pal7,GGCTTAGTACTAGTACTAAGC,synthetic
218
+ cut,S. pyogenes,Zhang,"<a href=""/68466/"">68466</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26145478/"">26145478</a>",PAX6,GATACTGGAGAAGCAGGGGC,H. sapiens
219
+ cut,S. pyogenes,Zhang,"<a href=""/68465/"">68465</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26145478/"">26145478</a>",PAX6,GTAGATTTTGTATGCACTGC,H. sapiens
220
+ interfere,S. pyogenes,Lu,"<a href=""/68900/"">68900</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",PcfiA,AAACAAAACCTCATCAGGCA,
221
+ interfere,S. pyogenes,Lu,"<a href=""/68899/"">68899</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26918244/"">26918244</a>",PcfiA,GAAGCTCACTCCTTAGCACG,
222
+ interfere,S. pyogenes,Lu,"<a href=""/74068/"">74068</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pck,AGGGCGAGGCGCCGACCAAGAATA,C. glutamicum
223
+ interfere,S. pyogenes,Lu,"<a href=""/74069/"">74069</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pck,TCCAGTTCAGCAGTTCCTTATTCT,C. glutamicum
224
+ cut,S. pyogenes,Kamoun,"<a href=""/46966/"">46966</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23929340/"">23929340</a>",PDS,GCCGTTAATTTGAGAGTCCA,N. benthamiana
225
+ cut,S. pyogenes,Kamoun,"<a href=""/49772/"">49772</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24112467/"">24112467</a>",PDS1,"multiple, see article",N. benthamiana
226
+ cut,S. pyogenes,Sheen,"<a href=""/52255/"">52255</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23929339/"">23929339</a>",PDS3,GGACTTTTGCCAGCCATGGT,A. thaliana
227
+ interfere,S. pyogenes,Lu,"<a href=""/74066/"">74066</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pgi,TGACCGATCATTACTCAAACTTCC,C. glutamicum
228
+ interfere,S. pyogenes,Lu,"<a href=""/74067/"">74067</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pgi,TTGCCTGGAAGTTTGAGTAATGAT,C. glutamicum
229
+ activate,S. pyogenes,Gersbach,"<a href=""/60719/"">60719</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25664691/"">25664691</a>",pGL3-Basic-8x-gRNA-eGFP reporter,AAAGGTCGAGAAACTGCAAA,synthetic
230
+ cut,S. pyogenes,Ward,"<a href=""/61252/"">61252</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25491644/"">25491644</a>",pha-1,ATGAATAACTTGATGAACAT,C. elegans
231
+ cut,S. pyogenes,Kim,"<a href=""//""></a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26479191/"">26479191</a>",PHYB,CACTAGGAGCAACACCCAA,A. thaliana
232
+ cut,S. pyogenes,Wolfe,"<a href=""/69240/"">69240</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",PLXNB2,GGGCCCAACCTAGGGCATGG,H. sapiens
233
+ cut,S. pyogenes,Patron,"<a href=""/68253/"">68253</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26616834/"">26616834</a>",PM19,GCTCTCCACTCTGGGCTCTT,H. vulgare
234
+ cut,S. pyogenes,Patron,"<a href=""/68254/"">68254</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26616834/"">26616834</a>",PM19,GGCTGGCGTTGGTCGTAACA,H. vulgare
235
+ cut,S. pyogenes,Shaw,"<a href=""/79004/"">79004</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26944679/"">26944679</a>",Prkaa1,TTATTGTCACAGGCATATGG,M. musculus
236
+ cut,S. pyogenes,Shaw,"<a href=""/79005/"">79005</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26944679/"">26944679</a>",Prkaa2,ACAGGCATATGGTTGTCCAT,M. musculus
237
+ cut,S. pyogenes,Schmitt-Ulms,"<a href=""/61857/"">61857</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25490046/"">25490046</a>",Prnp,GGTGGAACACCGGTGGAAGC,M. musculus
238
+ cut,S. pyogenes,Schmitt-Ulms,"<a href=""/61856/"">61856</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25490046/"">25490046</a>",Prnp,TTGGCCCCATCCACCGCCAT,M. musculus
239
+ cut,N. meningitidis,Church,"<a href=""/48651/"">48651</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer A,TACCATCTCAAGCTTGTTGA,Synthetic
240
+ cut,S. thermophilus,Church,"<a href=""/48653/"">48653</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer A,TACCATCTCAAGCTTGTTGA,Synthetic
241
+ cut,T. denticola,Church,"<a href=""/48655/"">48655</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer A,TACCATCTCAAGCTTGTTGA,Synthetic
242
+ cut,N. meningitidis,Church,"<a href=""/48652/"">48652</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer B,ACTTTAAAAGTATTCGCCAT,Synthetic
243
+ cut,S. thermophilus,Church,"<a href=""/48654/"">48654</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer B,ACTTTAAAAGTATTCGCCAT,Synthetic
244
+ cut,T. denticola,Church,"<a href=""/48656/"">48656</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24076762/"">24076762</a>",Protospacer B,ACTTTAAAAGTATTCGCCAT,Synthetic
245
+ cut,S. pyogenes,Moffat,"<a href=""/74184/"">74184</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",PSMB2,ATGTTCTTGTCGCCTCCGAC,H. sapiens
246
+ cut,S. pyogenes,Moffat,"<a href=""/74185/"">74185</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",PSMB2,AATATTGTCCAGATGAAGGA,H. sapiens
247
+ cut,S. pyogenes,Moffat,"<a href=""/74180/"">74180</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",PSMD1,TGTGCGCTACGGAGCTGCAA,H. sapiens
248
+ cut,S. pyogenes,Moffat,"<a href=""/74181/"">74181</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26627737/"">26627737</a>",PSMD1,ACCAGAGCCACAATAAGCCA,H. sapiens
249
+ cut,S. pyogenes,Jacks,"<a href=""/59909/"">59909</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25119044/"">25119044</a>",Pten,AGATCGTTAGCAGAAACAAA,M. musculus
250
+ cut,S. pyogenes,Xue,"<a href=""/66588/"">66588</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",Pten,GCTGTAGTAATATCTGCTAT,M. musculus
251
+ cut,S. pyogenes,Xue,"<a href=""/66587/"">66587</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26018130/"">26018130</a>",Pten,GTTTCATAGCGGCCACGAAGT,M. musculus
252
+ interfere,S. pyogenes,Lu,"<a href=""/74070/"">74070</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pyk,AGATTGTATGTACCCTAGGCCCAG,C. glutamicum
253
+ interfere,S. pyogenes,Lu,"<a href=""/74072/"">74072</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",pyk,ATTCCATCTGCACTAGCCACCGCT,C. glutamicum
254
+ tag,S. pyogenes,Mendenhall,"<a href=""/64057/"">64057</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26355004/"">26355004</a>",RAD21,CCAAGGTTCCATATTATATAAGG,H. sapiens
255
+ cut,S. pyogenes,Fire,"<a href=""/59927/"">59927</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",rde-1(D718),TGCCATTAACTATGTATGT,C. elegans
256
+ cut,S. pyogenes,Fire,"<a href=""/59928/"">59928</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",rde-1(D801),GATATTGTAGTCTATCGAGA,C. elegans
257
+ cut,S. pyogenes,Fire,"<a href=""/59929/"">59929</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",rde-1(H974),GATAAATGAGCATAATGAAC,C. elegans
258
+ interfere,S. pyogenes,Lu,"<a href=""/74075/"">74075</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26829286/"">26829286</a>",rfp,AAGTTCGTATGGAAGGTTCCGTTAA,
259
+ cut,S. pyogenes,Joung,"<a href=""/42246/"">42246</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",rgs4,GGAGAAGGTGAAGGACACTG,D. rerio
260
+ cut,S. pyogenes,Bornhauser,"<a href=""/75165/"">75165</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27194728/"">27194728</a>",RIP3,TACACGAGTGATGGTTCGGT,H. sapiens
261
+ cut,S. pyogenes,Bornhauser,"<a href=""/75163/"">75163</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27194728/"">27194728</a>",RIPK1,GCTCTGCTGGGAAGCGAATC,H. sapiens
262
+ cut,S. pyogenes,Joung,"<a href=""/47509/"">47509</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",RNF2,GTCATCTTAGTCATTACCTG,H. sapiens
263
+ cut,S. pyogenes,Fire,"<a href=""/59930/"">59930</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",rol-6(su1006),GTGAGACGTCAACAATATGG,C. elegans
264
+ cut,S. pyogenes,Kuhn,"<a href=""/64216/"">64216</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25803306/"">25803306</a>",Rosa26,ACTCCAGTCTTTCTAGAAGA,M.musculus
265
+ cut,S. pyogenes,Marraffini,"<a href=""/44505/"">44505</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360965/"">23360965</a>",rpsL,AAAAAACCGAACTCCGCGCTGCGTAAAGTA,E. coli
266
+ interfere,S. pyogenes,Voigt,"<a href=""/62285/"">62285</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25422271/"">25422271</a>",scramble,AACCCCTGATTGTATCCGCA,synthetic
267
+ cut,S. pyogenes,Chen,"<a href=""/47930/"">47930</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23918387/"">23918387</a>",slc24a5 (golden),GGTCTCTCGCAGGATGTTGC,D. rerio
268
+ cut,S. pyogenes,Hanna,"<a href=""/59725/"">59725</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25543152/"">25543152</a>",SOX17,GCCCAGCCCGGCCATCACCG,H. sapiens
269
+ activate,S. pyogenes,Wolfe,"<a href=""/50925/"">50925</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Sox17,GCTCCGGCTAGTTTTCCCGG,H. sapiens
270
+ activate,S. pyogenes,Wolfe,"<a href=""/50924/"">50924</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Sox17,GGAGGGGCAAGGGGCGGGCG,H. sapiens
271
+ activate,S. pyogenes,Wolfe,"<a href=""/50926/"">50926</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Sox17,GGGCAAGTACGTCGATTCCA,H. sapiens
272
+ activate,S. pyogenes,Wolfe,"<a href=""/50923/"">50923</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24346702/"">24346702</a>",Sox17,GGGCGTGGGCCTAACGACGC,H. sapiens
273
+ cut,S. pyogenes,Fire,"<a href=""/59935/"">59935</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",sqt-1,GGAAGGACATAGTTGTCAT,C. elegans
274
+ cut,S. pyogenes,Fire,"<a href=""/60212/"">60212</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",sqt-1,TGTGGAGTTGGGGTAGCGT,C. elegans
275
+ activate/interfere,S. pyogenes,Qi,"<a href=""/62338/"">62338</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",sv40 promoter,CATACTTCTGCCTGCTGGGGAGCCTG,synthetic
276
+ scaffold,S. pyogenes,Qi &amp; Lim,"<a href=""/62340/"">62340</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",SV40.NT1,GAATAGCTCAGAGGCCGAGG,H. sapiens
277
+ cut,S. pyogenes,Seydoux,"<a href=""/66100/"">66100</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",swan-2,ACAAATTGATATCCAATCA,C. elegans
278
+ cut,S. pyogenes,Seydoux,"<a href=""/66099/"">66099</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25249454/"">25249454</a>",swan-2,TGAAGAAAGTTATACTCGA,C. elegans
279
+ cut,S. pyogenes,Hanna,"<a href=""/59726/"">59726</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25543152/"">25543152</a>",T (BRACHYURY),CACGCGCAGTTCGCGCTCTG,H. sapiens
280
+ cut,S. pyogenes,"Guigo, Johnson","<a href=""/72627/"">72627</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",TCRC,gRNA1:GGGGAGCGGGAAAGCGGTCG; gRNA2:AACTGACCTTCAGGCCCGTA,H. sapiens
281
+ cut,N. meningitidis,Thomson,"<a href=""/47869/"">47869</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23940360/"">23940360</a>",tdtomato,CGAAATGAGAAAGGGAGCTACAAC,Synthetic
282
+ scaffold,S. pyogenes,Qi &amp; Lim,"<a href=""/62320/"">62320</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",TEF,TTGATATTTAAGTTAATAAA,S. cerevisiae
283
+ interfere,S. pyogenes,Weissman,"<a href=""/46922/"">46922</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",TEF1,TTGATATTTAAGTTAATAAA,S. cerevisiae
284
+ visualize,S. pyogenes,Qi,"<a href=""/51024/"">51024</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24360272/"">24360272</a>",telomeres,GTTAGGGTTAGGGTTAGGGTTA,H. sapiens
285
+ scaffold,S. pyogenes,Qi &amp; Lim,"<a href=""/62313/"">62313</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",TET,ACTTTTCTCTATCACTGATA,S. cerevisiae
286
+ interfere,S. pyogenes,Weissman,"<a href=""/46923/"">46923</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23849981/"">23849981</a>",TET promoter,TATCAGTGATAGAGAAAAGT,
287
+ activate/interfere,S. pyogenes,Qi,"<a href=""/62327/"">62327</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",Tet3G,GTACGTTCTCTATCACTGATA,synthetic
288
+ cut,S. pyogenes,Shaw,"<a href=""/79006/"">79006</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26944679/"">26944679</a>",Tfeb,AGCACTGTTGCCGGCCGAGG,M. musculus
289
+ RNA targeting,S. pyogenes,Yeo,"<a href=""/74709/"">74709</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26997482/"">26997482</a>",TFRC,GCACUGACCAGAUAAGAAUG,H. sapiens
290
+ cut,S. pyogenes,"Guigo, Johnson","<a href=""/72626/"">72626</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",TFRC,"gRNA1:GCAGCCTCAGAAATACAAAA, gRNA2CGGGATATCGGGTGGCGGCT",H. sapiens
291
+ cut,S. pyogenes,Wolfe,"<a href=""/69239/"">69239</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",TGM2,CCTGGACCCAACGCCCCAGG,H. sapiens
292
+ cut,S. pyogenes,Sibley,"<a href=""/59855/"">59855</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25480939/"">25480939</a>",TGME49_290860,ATTGGTCTGACAGCGATGTG,T. gondii
293
+ cut,S. pyogenes,Joung,"<a href=""/42247/"">42247</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",th,GGATGCGCGTAAGGAGCGCG,D. rerio
294
+ cut,S. pyogenes,Du,"<a href=""/65565/"">65565</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25849248/"">25849248</a>",th,GGGTTGTTACACTCATAAAG,D. rerio
295
+ cut,S. pyogenes,Joung,"<a href=""/42248/"">42248</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",tia1l,GGTATGTCGGGAACCTCTCC,D. rerio
296
+ cut,S. pyogenes,Joung,"<a href=""/42249/"">42249</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23360964/"">23360964</a>",tph1a,GGGAAAACACAACCGCAGCC,D. rerio
297
+ scaffold,S. pyogenes,Qi &amp; Lim,"<a href=""/62325/"">62325</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25533786/"">25533786</a>",TRE3G,GTACGTTCTCTATCACTGATA,H. sapiens
298
+ cut,S. pyogenes,Jin,"<a href=""/64331/"">64331</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25281382/"">25281382</a>",trp1,GTCAATTGTTCTCTTTCTAT,S. cerevisiae
299
+ cut,S. pyogenes,Jacks,"<a href=""/59910/"">59910</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25119044/"">25119044</a>",Trp53,CCTCGAGCTCCCTCTGAGCC,M. musculus
300
+ cut,S. pyogenes,Wolfe,"<a href=""/69234/"">69234</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",TS2,GACCCCCTCCACCCCGCCTC,H. sapiens
301
+ cut,S. pyogenes,Wolfe,"<a href=""/69235/"">69235</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",TS3,GGTGAGTGAGTGTGTGCGTG,H. sapiens
302
+ cut,S. pyogenes,Wolfe,"<a href=""/69236/"">69236</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26480473/"">26480473</a>",TS4,GAGTCCGAGCAGAAGAAGAA,H. sapiens
303
+ cut,S. pyogenes,Goldstein,"<a href=""/47550/"">47550</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23995389/"">23995389</a>",ttTi5605 Mos1 insertion site,ATATCAGTCTGTTTCGTAA,C. elegans
304
+ cut,S. pyogenes,Chen,"<a href=""/46761/"">46761</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23918387/"">23918387</a>",tyr,CCCCAGAAGTCCTCCAGTCC,D. rerio
305
+ cut,S. pyogenes,Chen,"<a href=""/64250/"">64250</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25855067/"">25855067</a>",tyr,GGACTGGAGGACTTCTGGGG,D. rerio
306
+ cut,S. pyogenes,Mashimo,"<a href=""/60966/"">60966</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Tyr (albino),TTTCCAGGATTATGTAATAG,R. norvegicus
307
+ cut,S. pyogenes,Mashimo,"<a href=""/60967/"">60967</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24967838/"">24967838</a>",Tyr (wild type),TTTCCAGGATTACGTAATAG,R. norvegicus
308
+ cut,S. pyogenes,"Guigo, Johnson","<a href=""/72628/"">72628</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/26493208/"">26493208</a>",UCA1,gRNA1:GGTTTCCTTTTAGATGACGG; gRNA2:TCTGAAAAGAGAGTCAGCGA,H. sapiens
309
+ cut,S. pyogenes,Fire,"<a href=""/59932/"">59932</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",unc-109(n499),GGAACTCGTGTCAAAACAAC,C. elegans
310
+ cut,S. pyogenes,Calarco,"<a href=""/46169/"">46169</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23817069/"">23817069</a>",unc-119,GAATTTTCTGAAATTAAAGA,C. elegans
311
+ cut,S. pyogenes,Mello,"<a href=""/58202/"">58202</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24879462/"">24879462</a>",unc-22,GAACCCGTTGCCGAATACAC,C. elegans
312
+ cut,S. pyogenes,Fire,"<a href=""/59931/"">59931</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25161212/"">25161212</a>",unc-58(e665),TCCACGCACATGGTCACTA,C. elegans
313
+ cut,S. pyogenes,Sibley,"<a href=""/54467/"">54467</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24825012/"">24825012</a>",UPRT,GGCGTCTCGATTGTGAGAGC,Toxoplasma gondii
314
+ cut,S. pyogenes,Jin,"<a href=""/64330/"">64330</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25281382/"">25281382</a>",ura3,GAGTAAAAAATTGTACTTGG,S. cerevisiae
315
+ cut,S. pyogenes,Ye,"<a href=""/61812/"">61812</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24424410/"">24424410</a>",USP13,GCCGCCCGGCATGCCGAACA,H. sapiens
316
+ cut,S. pyogenes,Joung,"<a href=""/47506/"">47506</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",VEGF,GACCCCCTCCACCCCGCCTC,H. sapiens
317
+ cut,S. pyogenes,Joung,"<a href=""/47505/"">47505</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",VEGF,GGGTGGGGGGAGTTTGCTCC,H. sapiens
318
+ cut,S. pyogenes,Joung,"<a href=""/47507/"">47507</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/23792628/"">23792628</a>",VEGF,GGTGAGTGAGTGTGTGCGTG,H. sapiens
319
+ interfere,S. pyogenes,Voigt,"<a href=""/62286/"">62286</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25422271/"">25422271</a>",VR,TGCGCTCGGTCGTTCGGCTG,synthetic
320
+ nick,S. pyogenes,Yang,"<a href=""/73228/"">73228</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/27213844/"">27213844</a>",xylR,CGAGTTAGACATAATAGTGA,C. beijerinckii
321
+ cut,S. pyogenes,Ward,"<a href=""/61250/"">61250</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/25491644/"">25491644</a>",Y61A9LA.1,GGATGGATGTGTAGTCAATT,C. elegans
322
+ cut,S. pyogenes,Liu,"<a href=""/49331/"">49331</a>","<a href=""https://www.ncbi.nlm.nih.gov/pubmed/24326186/"">24326186</a>",yellow,GGTTTTGGACACTGGAACCG,D. melanogaster
Biomanus_upload/biomni/know_how/sgRNA_design_guide.md ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # sgRNA Design Guide: Three-Tiered Approach
2
+
3
+ ---
4
+
5
+ ## Metadata
6
+
7
+ **Short Description**: Comprehensive guide for finding or designing sgRNAs using validated sequences, CRISPick datasets, or de novo design tools.
8
+
9
+ **Authors**: Biomni Team
10
+
11
+ **Version**: 1.0
12
+
13
+ **Last Updated**: November 2025
14
+
15
+ **License**: CC BY 4.0
16
+
17
+ **Commercial Use**: ✅ Allowed
18
+
19
+ ## Citations and Acknowledgments
20
+
21
+ ### If you use validated sgRNAs from our database (Option 1):
22
+ - **Database Source**: Addgene (https://www.addgene.org)
23
+ - **Citation**: Always cite the original publication associated with each sgRNA using the PubMed ID provided in the database
24
+ - **Acknowledgment**: "Validated sgRNA sequences obtained from Addgene (https://www.addgene.org/crispr/reference/grna-sequence/)"
25
+
26
+ ### If you use CRISPick designs (Option 2):
27
+ - **Acknowledgment Statement**: "Guide designs provided by the CRISPick web tool of the GPP at the Broad Institute"
28
+ - **Citation for Cas9 designs (SpCas9, SaCas9)**: Sanson KR, et al. Optimized libraries for CRISPR-Cas9 genetic screens with multiple modalities. Nat Commun. 2018;9(1):5416. PMID: 30575746
29
+ - **Citation for Cas12a designs (AsCas12a, enAsCas12a)**: DeWeirdt PC, et al. Optimization of AsCas12a for combinatorial genetic screens in human cells. Nat Biotechnol. 2021;39(1):94-104. PMID: 32661438
30
+ - Note: This paper describes enAsCas12a optimization; specify which variant you used in your methods
31
+
32
+ ---
33
+
34
+ ## Overview
35
+
36
+ This guide provides a three-tiered approach to sgRNA design, prioritizing validated sequences before moving to computational predictions. Always start with Option 1 and proceed to subsequent options only if needed.
37
+
38
+
39
+
40
+ ## Option 1: Search Validated sgRNA Sequences (Recommended First)
41
+
42
+ ### 1.1 Search for Validated sgRNAs
43
+
44
+ **IMPORTANT**: You MUST complete BOTH Method 1 AND Method 2 before proceeding to Option 2. Do not skip Method 2 even if Method 1 finds no results.
45
+
46
+ #### Method 1: Search Our Database (Fastest)
47
+
48
+ We maintain a curated database of 300+ validated sgRNA sequences from Addgene with experimental evidence.
49
+
50
+ **Location**: `biomni/know_how/resource/addgene_grna_sequences.csv`
51
+
52
+ **Search the database**:
53
+ ```python
54
+ import pandas as pd
55
+
56
+ # Load the database
57
+ df = pd.read_csv('addgene_grna_sequences.csv')
58
+
59
+ # Search for your gene
60
+ gene_name = "TP53"
61
+ results = df[df['Target_Gene'].str.upper() == gene_name.upper()]
62
+
63
+ # Filter by species and application
64
+ results_filtered = results[
65
+ (results['Target_Species'] == 'H. sapiens') &
66
+ (results['Application'] == 'cut') # or 'activate', 'RNA targeting'
67
+ ]
68
+
69
+ # Display results with references
70
+ print(results_filtered[['Target_Gene', 'Target_Sequence',
71
+ 'Plasmid_ID', 'PubMed_ID', 'Depositor']])
72
+ ```
73
+
74
+ **Database columns**:
75
+ - `Target_Gene`: Gene symbol
76
+ - `Target_Species`: Organism (H. sapiens, M. musculus, etc.)
77
+ - `Target_Sequence`: 20bp sgRNA sequence (5' to 3')
78
+ - `Application`: cut (knockout), activate (CRISPRa), RNA targeting (CRISPRi)
79
+ - `Cas9_Species`: S. pyogenes, S. aureus, etc.
80
+ - `Plasmid_ID`: Addgene plasmid number
81
+ - `Plasmid_URL`: Direct link to plasmid page
82
+ - `PubMed_ID`: **Publication reference** (cite this in your work)
83
+ - `PubMed_URL`: Direct link to paper
84
+ - `Depositor`: Research lab that contributed the sequence
85
+
86
+ #### Method 2: Advanced Web Search (REQUIRED - Do Not Skip)
87
+
88
+ **CRITICAL**: Even if Method 1 found no results, you MUST perform this literature search before moving to Option 2. Many validated sgRNAs are published in literature but not in the Addgene database.
89
+
90
+ Use `advanced_web_search_claude` from `biomni.tool.literature` to find validated sgRNAs from literature and databases:
91
+
92
+ ```python
93
+ from biomni.tool.literature import advanced_web_search_claude
94
+
95
+ # Example usage
96
+ results = advanced_web_search_claude("sgRNA TP53 validated H. sapiens experimental")
97
+ ```
98
+
99
+ **Search queries to try (use multiple):**
100
+ ```
101
+ "sgRNA" OR "guide RNA" "[GENE_NAME]" validated experimental
102
+ "CRISPR knockout" "[GENE_NAME]" sgRNA sequence validated
103
+ "[GENE_NAME]" sgRNA "cutting efficiency" OR "on-target"
104
+ "[GENE_NAME]" "guide sequence" CRISPR validated
105
+ ```
106
+
107
+ **Example for TP53:**
108
+ ```
109
+ "sgRNA" "TP53" validated "H. sapiens" experimental
110
+ "CRISPR knockout" "TP53" guide sequence validated
111
+ ```
112
+
113
+ **What to search for in results:**
114
+ - Published papers with validated sgRNA sequences
115
+ - Supplementary materials containing sgRNA sequences
116
+ - Other CRISPR databases (e.g., GenScript, Horizon Discovery)
117
+ - Laboratory protocols with specific sgRNA sequences
118
+
119
+ **IMPORTANT**: Spend adequate time searching literature. Look through at least the first 10-15 search results and check supplementary materials of relevant papers.
120
+
121
+ #### What to Do with Results:
122
+
123
+ ✅ **If you find matching sgRNAs (from either method):**
124
+ 1. **Record the sgRNA sequence** (20bp target sequence)
125
+ 2. **Note the reference**:
126
+ - From database: Use `PubMed_ID` to cite the original paper
127
+ - From web search: Record the publication DOI/PubMed ID
128
+ 3. **Record validation details**: Cell line, cutting efficiency, any reported off-targets
129
+
130
+
131
+ **Example result format:**
132
+ ```
133
+ Gene: TP53
134
+ sgRNA sequence: GAGGTTGTGAGGCGCTGCCC
135
+ Species: H. sapiens (human)
136
+ Application: Knockout (cut)
137
+ Reference: PubMed ID 24336569 (Ran et al., 2013)
138
+ Validation: Tested in HEK293T cells, 85% cutting efficiency
139
+ ```
140
+
141
+ ❌ **If no matches found in BOTH Method 1 AND Method 2:**
142
+ Only then proceed to **Option 2: Download CRISPick Dataset**
143
+
144
+
145
+
146
+ ## Option 2: Download Pre-Computed sgRNAs from CRISPick
147
+
148
+ ### When to Use This Option?
149
+ - No validated sgRNAs found in Addgene
150
+ - Need comprehensive coverage of a gene
151
+ - Want multiple sgRNA options with predicted scores
152
+ - Working with genome-scale screening
153
+
154
+ ### 2.1 Overview of CRISPick
155
+
156
+ **CRISPick** (from Broad Institute GPP) provides pre-computed sgRNA designs for entire genomes with 238 available datasets covering:
157
+ - Human (GRCh38, GRCh37)
158
+ - Mouse (GRCm38, GRCm39)
159
+ - Dog, Cow, Monkey, and other model organisms
160
+ - Multiple Cas enzymes (SpCas9, SaCas9, AsCas12a, enAsCas12a)
161
+ - **IMPORTANT**: AsCas12a and enAsCas12a are DIFFERENT enzymes
162
+ - **AsCas12a**: Wild-type Acidaminococcus sp. Cas12a
163
+ - **enAsCas12a**: Enhanced variant with improved activity
164
+ - **Critical**: sgRNAs designed for enAsCas12a may NOT work with wild-type AsCas12a
165
+ - Always match the dataset to your specific Cas12a variant
166
+ - Different applications (knockout, activation, inhibition)
167
+
168
+ ### 2.2 Find the Download Link
169
+
170
+ **All 238 download links are available in**: `biomni/know_how/resource/CRISPick_download_links.txt`
171
+
172
+ #### Step 1: Understand File Naming Convention
173
+
174
+ Files are named: `sgRNA_design_{TAXID}_{GENOME}_{CAS}_{APPLICATION}_{ALGORITHM}_{SOURCE}_{DATE}.txt.gz`
175
+
176
+ **Common datasets:**
177
+
178
+ | Organism | Genome | Cas9 | Application | Search Pattern |
179
+ |-|--||-|-|
180
+ | Human | GRCh38 | SpCas9 | Knockout | `9606_GRCh38_SpyoCas9_CRISPRko` |
181
+ | Human | GRCh38 | SpCas9 | Activation | `9606_GRCh38_SpyoCas9_CRISPRa` |
182
+ | Human | GRCh38 | SaCas9 | Knockout | `9606_GRCh38_SaurCas9_CRISPRko` |
183
+ | Mouse | GRCm38 | SpCas9 | Knockout | `10090_GRCm38_SpyoCas9_CRISPRko` |
184
+ | Mouse | GRCm38 | SpCas9 | Activation | `10090_GRCm38_SpyoCas9_CRISPRa` |
185
+
186
+ **Key components:**
187
+ - **TAXID**: `9606` (Human), `10090` (Mouse), `9615` (Dog), `9913` (Cow)
188
+ - **CAS**:
189
+ - `SpyoCas9` (SpCas9, NGG PAM)
190
+ - `SaurCas9` (SaCas9, NNGRRT PAM)
191
+ - `AsCas12a` (Wild-type Cas12a, TTTV PAM)
192
+ - `enAsCas12a` (Enhanced Cas12a, TTTV PAM)
193
+ - **WARNING**: Do NOT use AsCas12a datasets if you have enAsCas12a, and vice versa
194
+ - **APPLICATION**: `CRISPRko` (knockout), `CRISPRa` (activation), `CRISPRi` (inhibition)
195
+
196
+ #### Step 2: Find Your Download URL
197
+
198
+ ```bash
199
+ # Search the download links file
200
+ grep "9606_GRCh38_SpyoCas9_CRISPRko" biomni/know_how/resource/CRISPick_download_links.txt
201
+
202
+ # Or for mouse
203
+ grep "10090_GRCm38_SpyoCas9_CRISPRko" biomni/know_how/resource/CRISPick_download_links.txt
204
+ ```
205
+
206
+ #### Step 3: Download and Extract
207
+
208
+ ```bash
209
+ # Copy the URL from download_links.txt, then:
210
+ wget [PASTE_URL_HERE]
211
+
212
+ # Extract the file
213
+ gunzip sgRNA_design_*.txt.gz
214
+ ```
215
+
216
+ **File sizes**: Knockout (300-700 MB), Activation (50-100 MB), Summary files (1-3 MB)
217
+
218
+ ### 2.4 Understanding the File Format
219
+
220
+ The `.txt` file is tab-delimited. Column names differ between knockout and activation/inhibition datasets.
221
+
222
+ **Essential Columns (All files):**
223
+ - **sgRNA Sequence**: The 20bp guide RNA sequence (5' to 3')
224
+ - **Target Gene Symbol**: Gene name (e.g., "TP53", "BRCA1")
225
+ - **Combined Rank**: Overall ranking (lower = better) - **Use this by default**
226
+ - **On-Target Rank**: Ranking by efficiency only (lower = better)
227
+ - **Off-Target Rank**: Ranking by specificity only (lower = better)
228
+ - **PAM Sequence**: The PAM sequence (e.g., "AGG", "TGG")
229
+
230
+ **Knockout-specific columns:**
231
+ - **sgRNA Cut Position (1-based)**: Genomic coordinate of cut site
232
+ - **Exon Number**: Which exon is targeted
233
+ - **Target Cut %**: Percentage of protein affected
234
+
235
+ **Activation/Inhibition-specific columns:**
236
+ - **sgRNA 'Cut' Position**: Position relative to gene
237
+ - **sgRNA 'Cut' Site TSS Offset**: Distance from transcription start site (bp)
238
+ - **DHS Score**: DNase hypersensitivity score (for CRISPRa)
239
+
240
+ ### 2.5 Extract and Select sgRNAs
241
+
242
+ #### Step 1: Load Data and Filter for Your Gene
243
+
244
+ ```python
245
+ import pandas as pd
246
+
247
+ # Load the dataset
248
+ df = pd.read_csv('sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_*.txt',
249
+ sep='\t', low_memory=False)
250
+
251
+ # Filter for your gene
252
+ gene_name = "TP53"
253
+ gene_sgrnas = df[df['Target Gene Symbol'] == gene_name].copy()
254
+
255
+ print(f"Found {len(gene_sgrnas)} sgRNAs for {gene_name}")
256
+ ```
257
+
258
+ #### Step 2: Select Top sgRNAs
259
+
260
+ **Default: Use Combined Rank (balances efficiency and specificity)**
261
+ ```python
262
+ # Sort by Combined Rank (lower is better)
263
+ top_sgrnas = gene_sgrnas.nsmallest(10, 'Combined Rank')
264
+
265
+ print(top_sgrnas[['sgRNA Sequence', 'Combined Rank',
266
+ 'Exon Number', 'sgRNA Cut Position (1-based)']])
267
+ ```
268
+
269
+ **Option A: Prioritize On-Target Efficiency**
270
+ ```python
271
+ # Sort by On-Target Rank (for maximum cutting efficiency)
272
+ efficient_sgrnas = gene_sgrnas.nsmallest(10, 'On-Target Rank')
273
+ ```
274
+
275
+ **Option B: Prioritize Off-Target Specificity**
276
+ ```python
277
+ # Sort by Off-Target Rank (for maximum specificity)
278
+ specific_sgrnas = gene_sgrnas.nsmallest(10, 'Off-Target Rank')
279
+ ```
280
+
281
+ #### Step 3: Filter by Custom Criteria (Optional)
282
+
283
+ **Filter by Exon Number:**
284
+ ```python
285
+ # Target specific exon (e.g., exon 5)
286
+ exon5_sgrnas = gene_sgrnas[gene_sgrnas['Exon Number'] == 5]
287
+ top_exon5 = exon5_sgrnas.nsmallest(5, 'Combined Rank')
288
+ ```
289
+
290
+ **Filter by Genomic Position:**
291
+ ```python
292
+ # Target specific genomic range
293
+ position_filtered = gene_sgrnas[
294
+ (gene_sgrnas['sgRNA Cut Position (1-based)'] >= 7572000) &
295
+ (gene_sgrnas['sgRNA Cut Position (1-based)'] <= 7575000)
296
+ ]
297
+ ```
298
+
299
+ **Target Early Exons for Knockout:**
300
+ ```python
301
+ # Get sgRNAs from first 3 exons
302
+ early_exons = gene_sgrnas[gene_sgrnas['Exon Number'] <= 3]
303
+ top_early = early_exons.nsmallest(10, 'Combined Rank')
304
+ ```
305
+
306
+ **Filter by Target Cut Percentage:**
307
+ ```python
308
+ # Target sgRNAs that affect significant portion of protein
309
+ high_impact = gene_sgrnas[gene_sgrnas['Target Cut %'] <= 50] # Cut in first 50%
310
+ top_high_impact = high_impact.nsmallest(10, 'Combined Rank')
311
+ ```
312
+
313
+ #### Step 4: Select Multiple sgRNAs for Validation
314
+
315
+ ```python
316
+ # Get top 4 sgRNAs from different exons for redundancy
317
+ final_selection = gene_sgrnas.sort_values('Combined Rank').groupby('Exon Number').head(1).head(4)
318
+
319
+ # Save results
320
+ final_selection.to_csv(f'{gene_name}_selected_sgRNAs.csv', index=False)
321
+
322
+ print("\nSelected sgRNAs:")
323
+ print(final_selection[['sgRNA Sequence', 'Exon Number', 'Combined Rank']])
324
+ ```
325
+
326
+ ### 2.6 What to Do with Results
327
+
328
+ ✅ **Once you have selected sgRNAs:**
329
+ 1. Choose **3-4 sgRNAs** (use Combined_Rank by default)
330
+
331
+ ❌ **If dataset doesn't cover your gene or organism:**
332
+ Proceed to **Option 3: De Novo sgRNA Design**
333
+
334
+
335
+
336
+ ## Option 3: General sgRNA Design Guidelines (Last Resort)
337
+
338
+ ### When to Use This Option?
339
+ - Gene not covered in CRISPick datasets
340
+ - Non-model organism
341
+ - Custom design requirements
342
+
343
+ ### General Design Rules
344
+
345
+ #### Essential Requirements:
346
+ 1. **Length**:
347
+ - 20 bp for SpCas9 and SaCas9
348
+ - 23-25 bp for Cas12a variants (AsCas12a, enAsCas12a)
349
+ 2. **PAM sequence**:
350
+ - **SpCas9**: Requires **NGG** immediately after target (3' end)
351
+ - **SaCas9**: Requires **NNGRRT** immediately after target (3' end)
352
+ - **AsCas12a** (wild-type): Requires **TTTV** before target (5' end)
353
+ - **enAsCas12a** (enhanced): Requires **TTTV** before target (5' end)
354
+ - **CRITICAL**: Guides designed for enAsCas12a may not work with wild-type AsCas12a due to different activity profiles
355
+ 3. **GC content**: 40-60% is optimal
356
+ 4. **Avoid**:
357
+ - TTTT sequences (terminates transcription)
358
+ - Long runs of same nucleotide (>4 repeats)
359
+
360
+ #### Target Location:
361
+ - **For knockout**: Target early exons (first 50% of gene)
362
+ - **For activation**: Target -200 to +1 bp from transcription start site (TSS)
363
+ - **For inhibition**: Target -50 to +300 bp from TSS
364
+
365
+ #### Best Practices:
366
+ - **Test 3-4 different sgRNAs** per target gene
367
+ - Select sgRNAs with high efficiency scores (>0.5) and minimal off-targets
368
+ - Validate experimentally with Sanger sequencing
369
+
370
+
371
+
372
+ ## Quick Start Examples
373
+
374
+ ### Example 1: Knockout TP53 in Human Cells
375
+
376
+ **Step 1**: Check Addgene
377
+ ```python
378
+ df = pd.read_csv('addgene_grna_sequences.csv')
379
+ tp53_results = df[(df['Target_Gene'] == 'TP53') &
380
+ (df['Target_Species'] == 'H. sapiens') &
381
+ (df['Application'] == 'cut')]
382
+ # Result: Found 0 entries → Proceed to Option 2
383
+ ```
384
+
385
+ **Step 2**: Download CRISPick dataset
386
+ ```bash
387
+ # Download human GRCh38 SpCas9 knockout dataset
388
+ wget https://portals.broadinstitute.org/gppx/public/sgrna_design/api/downloads/\
389
+ sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_RS3seq-Chen2013+RS3target_NCBI_20241104.txt.gz
390
+
391
+ gunzip sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_*.txt.gz
392
+ ```
393
+
394
+ **Step 3**: Extract TP53 sgRNAs
395
+ ```python
396
+ df = pd.read_csv('sgRNA_design_9606_GRCh38_SpyoCas9_CRISPRko_*.txt', sep='\t')
397
+ tp53 = df[df['Gene_Symbol'] == 'TP53']
398
+ top_sgrnas = tp53[
399
+ (tp53['sgRNA_score'] > 0.6) &
400
+ (tp53['Off_target_stringency'] > 0.5)
401
+ ].sort_values('sgRNA_score', ascending=False).head(4)
402
+
403
+ print(top_sgrnas[['sgRNA_sequence', 'sgRNA_score', 'Exon_ID']])
404
+ ```
405
+
406
+ ### Example 2: Activate OCT4 in Human iPSCs
407
+
408
+ **Step 1**: Check Addgene
409
+ ```python
410
+ oct4_results = df[(df['Target_Gene'] == 'OCT4') &
411
+ (df['Application'] == 'activate')]
412
+ # Found 1 validated sgRNA!
413
+ print(oct4_results['Target_Sequence'].values[0])
414
+ # Use this sequence ✅
415
+ ```
416
+
417
+
418
+
419
+ **Remember**: Always start with validated sequences (Option 1), then move to pre-computed designs (Option 2), and only use de novo design (Option 3) when necessary. Testing 3-4 sgRNAs per gene is standard practice regardless of prediction scores.
Biomanus_upload/biomni/know_how/single_cell_annotation.md ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Single Cell RNA-seq Cell Type Annotation
2
+
3
+ ---
4
+
5
+ ## Metadata
6
+
7
+ **Short Description**: Best practices for annotating cell types in single-cell RNA-seq data using marker-based, automated, and reference-based approaches.
8
+
9
+ **Authors**: Distilled from "Single-cell best practices" by Luecken, M.D. et al.
10
+
11
+ **Affiliations**: Helmholtz Munich, Wellcome Sanger Institute, Harvard Medical School, and contributors
12
+
13
+ **Version**: 1.0
14
+
15
+ **Last Updated**: January 2025
16
+
17
+ **License**: CC BY 4.0
18
+
19
+ **Commercial Use**: ✅ Allowed
20
+
21
+ **Source**: https://www.sc-best-practices.org/cellular_structure/annotation.html
22
+
23
+ **Citation**: Luecken, M.D., Theis, F.J. et al. (2023). Current best practices in single-cell RNA-seq analysis: a tutorial. Molecular Systems Biology.
24
+
25
+ ---
26
+
27
+ ## Overview
28
+
29
+ Cell type annotation is the process of assigning cell type labels to clusters or individual cells in single-cell RNA-seq data. This guide covers three main approaches and their practical implementation.
30
+
31
+ ## Three Annotation Approaches
32
+
33
+ ### 1. Manual Marker-Based Annotation
34
+ Identify cell types by examining expression of known marker genes in each cluster.
35
+
36
+ **Tools**: Scanpy, Seurat
37
+ **Best for**: Small datasets, novel cell types, high confidence needs
38
+
39
+ ### 2. Automated Annotation
40
+ Use pre-trained classifiers to automatically assign cell type labels.
41
+
42
+ **Tools**: CellTypist, scAnnotate
43
+ **Best for**: Standard tissues, quick preliminary annotation, large datasets
44
+
45
+ ### 3. Reference-Based Label Transfer
46
+ Transfer labels from annotated reference datasets to your query data.
47
+
48
+ **Tools**: scArches, scANVI, Azimuth, SingleR
49
+ **Best for**: Well-characterized tissues, integration with public data
50
+
51
+ ## Recommended Workflow
52
+
53
+ ### Step 1: Quality Control First
54
+ - **Remove low-quality cells before annotation**
55
+ - Filter doublets (expected doublet rate: 0.8% per 1000 cells)
56
+ - Check for ambient RNA contamination
57
+ - Verify cluster quality and resolution
58
+
59
+ ### Step 2: Initial Marker-Based Assessment
60
+
61
+ ```python
62
+ # Scanpy example
63
+ import scanpy as sc
64
+
65
+ # Calculate marker genes for clusters
66
+ sc.tl.rank_genes_groups(adata, 'leiden', method='wilcoxon')
67
+
68
+ # Visualize top markers
69
+ sc.pl.rank_genes_groups(adata, n_genes=25, sharey=False)
70
+
71
+ # Plot known markers
72
+ markers = {
73
+ 'T cells': ['CD3D', 'CD3E', 'CD4', 'CD8A'],
74
+ 'B cells': ['CD19', 'MS4A1', 'CD79A'],
75
+ 'Monocytes': ['CD14', 'FCGR3A', 'LYZ'],
76
+ 'NK cells': ['NCAM1', 'NKG7', 'GNLY']
77
+ }
78
+
79
+ sc.pl.dotplot(adata, markers, groupby='leiden')
80
+ ```
81
+
82
+ ### Step 3: Use Automated Tools for Validation
83
+
84
+ ```python
85
+ # CellTypist example (fast, accurate for immune cells)
86
+ import celltypist
87
+ from celltypist import models
88
+
89
+ # Download immune cell model
90
+ model = models.Model.load(model='Immune_All_Low.pkl')
91
+
92
+ # Predict cell types
93
+ predictions = celltypist.annotate(adata, model=model, majority_voting=True)
94
+ adata = predictions.to_adata()
95
+ ```
96
+
97
+ ### Step 4: Reference-Based Refinement
98
+
99
+ ```python
100
+ # scArches example for label transfer
101
+ import scarches as sca
102
+
103
+ # Load pre-trained reference model
104
+ model = sca.models.SCANVI.load_query_data(
105
+ adata=adata, # Your query data
106
+ reference_model="path/to/reference_model"
107
+ )
108
+
109
+ # Transfer labels
110
+ model.train(max_epochs=100)
111
+ adata.obs['transferred_labels'] = model.predict()
112
+ ```
113
+
114
+ ## Best Practices
115
+
116
+ ### Do's:
117
+ 1. **Always combine multiple approaches** - Use marker-based validation even with automated tools
118
+ 2. **Check cluster purity** - Ensure clusters represent single cell types
119
+ 3. **Validate with multiple marker sets** - Don't rely on single markers
120
+ 4. **Consider biological context** - Tissue type, disease state, developmental stage
121
+ 5. **Document confidence levels** - Note uncertain annotations
122
+ 6. **Use hierarchical annotation** - Broad categories first, then subtypes
123
+
124
+ ### Don'ts:
125
+ 1. **Don't over-cluster** - Too fine resolution creates artificial distinctions
126
+ 2. **Don't ignore batch effects** - Correct before annotation
127
+ 3. **Don't trust automation blindly** - Always validate predictions
128
+ 4. **Don't mix cell states with cell types** - Activated vs. resting cells are states, not types
129
+ 5. **Don't annotate low-quality cells** - Remove them first
130
+
131
+ ## Common Pitfalls
132
+
133
+ ### 1. Doublet Clusters
134
+ **Problem**: Clusters with markers from multiple cell types
135
+ **Solution**: Use doublet detection tools (Scrublet, DoubletFinder) before annotation
136
+
137
+ ### 2. Ambient RNA
138
+ **Problem**: Background markers in all cells
139
+ **Solution**: Use SoupX or CellBender for decontamination
140
+
141
+ ### 3. Over-interpretation of Small Clusters
142
+ **Problem**: Rare clusters may be technical artifacts
143
+ **Solution**: Require minimum cell count (e.g., >25 cells), validate with independent data
144
+
145
+ ### 4. Reference Mismatch
146
+ **Problem**: Reference from different tissue/species/condition
147
+ **Solution**: Use tissue-matched references, check marker overlap before transfer
148
+
149
+ ## Tool Selection Guide
150
+
151
+ | Scenario | Recommended Tool | Why |
152
+ |----------|------------------|-----|
153
+ | Immune cells (human) | CellTypist | Pre-trained on large immune atlases |
154
+ | Mouse tissues | scArches + Mouse Cell Atlas | Comprehensive mouse reference |
155
+ | Novel cell types | Manual + Scanpy/Seurat | Need domain expertise |
156
+ | Large datasets (>100k cells) | CellTypist | Fast, scalable |
157
+ | Cross-species | Manual markers | Limited reference transfer |
158
+ | Developmental data | scArches | Handles continuous states |
159
+
160
+ ## Key Marker Genes by Cell Type
161
+
162
+ ### Blood/Immune:
163
+ - **T cells**: CD3D, CD3E (all T cells); CD4, CD8A (subtypes)
164
+ - **B cells**: CD19, MS4A1 (CD20), CD79A
165
+ - **Monocytes/Macrophages**: CD14, CD68, LYZ
166
+ - **NK cells**: NCAM1 (CD56), NKG7, KLRD1
167
+ - **Dendritic cells**: FCER1A, CD1C
168
+
169
+ ### Epithelial:
170
+ - **General epithelial**: EPCAM, KRT18, KRT19
171
+ - **Lung AT1**: AGER, PDPN
172
+ - **Lung AT2**: SFTPC, SFTPA1
173
+ - **Intestinal**: VIL1, MUC2
174
+
175
+ ### Stromal:
176
+ - **Fibroblasts**: COL1A1, DCN, LUM
177
+ - **Endothelial**: PECAM1 (CD31), VWF, CDH5
178
+ - **Smooth muscle**: ACTA2, MYH11, TAGLN
179
+
180
+ ## Validation Checklist
181
+
182
+ - [ ] Cluster purity: >80% cells with same label per cluster
183
+ - [ ] Marker consistency: Top DE genes match expected markers
184
+ - [ ] Biological plausibility: Expected proportions for tissue type
185
+ - [ ] Cross-method agreement: Manual and automated annotations align
186
+ - [ ] Reference quality: >70% cells successfully transferred
187
+ - [ ] Doublet check: No clusters with multi-lineage markers
188
+ - [ ] Documentation: Record confidence levels and uncertain calls
189
+
190
+ ## Resources
191
+
192
+ ### Tools:
193
+ - **Scanpy**: https://scanpy.readthedocs.io/
194
+ - **CellTypist**: https://www.celltypist.org/
195
+ - **scArches**: https://scarches.readthedocs.io/
196
+ - **Seurat**: https://satijalab.org/seurat/
197
+
198
+ ### References:
199
+ - **PanglaoDB**: Database of marker genes
200
+ - **CellMarker**: Curated cell marker database
201
+ - **Human Cell Atlas**: Reference datasets
202
+
203
+ ### Pre-trained Models:
204
+ - **CellTypist models**: 30+ tissue-specific models
205
+ - **Azimuth references**: PBMC, lung, kidney, etc.
206
+ - **scArches models**: Multiple tissue references
207
+
208
+ ## Troubleshooting
209
+
210
+ **Issue**: All clusters look similar
211
+ → Increase clustering resolution, check if data is normalized
212
+
213
+ **Issue**: Too many small clusters
214
+ → Decrease resolution, merge similar clusters based on markers
215
+
216
+ **Issue**: Automated tool gives inconsistent results
217
+ → Check input normalization, try multiple tools, fall back to manual
218
+
219
+ **Issue**: Can't find clear markers for cluster
220
+ → May be transitional state, doublet, or low-quality cells
221
+
222
+ **Issue**: Reference transfer fails
223
+ → Check batch correction, ensure overlapping gene sets, verify tissue match
Biomanus_upload/biomni/llm.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import TYPE_CHECKING, Literal, Optional
3
+
4
+ from langchain_core.language_models.chat_models import BaseChatModel
5
+
6
+ if TYPE_CHECKING:
7
+ from biomni.config import BiomniConfig
8
+
9
+ SourceType = Literal["OpenAI", "AzureOpenAI", "Anthropic", "Ollama", "Gemini", "Bedrock", "Groq", "Custom"]
10
+ ALLOWED_SOURCES: set[str] = set(SourceType.__args__)
11
+
12
+
13
+ def get_llm(
14
+ model: str | None = None,
15
+ temperature: float | None = None,
16
+ stop_sequences: list[str] | None = None,
17
+ source: SourceType | None = None,
18
+ base_url: str | None = None,
19
+ api_key: str | None = None,
20
+ config: Optional["BiomniConfig"] = None,
21
+ ) -> BaseChatModel:
22
+ """
23
+ Get a language model instance based on the specified model name and source.
24
+ This function supports models from OpenAI, Azure OpenAI, Anthropic, Ollama, Gemini, Bedrock, and custom model serving.
25
+ Args:
26
+ model (str): The model name to use
27
+ temperature (float): Temperature setting for generation
28
+ stop_sequences (list): Sequences that will stop generation
29
+ source (str): Source provider: "OpenAI", "AzureOpenAI", "Anthropic", "Ollama", "Gemini", "Bedrock", or "Custom"
30
+ If None, will attempt to auto-detect from model name
31
+ base_url (str): The base URL for custom model serving (e.g., "http://localhost:8000/v1"), default is None
32
+ api_key (str): The API key for the custom llm
33
+ config (BiomniConfig): Optional configuration object. If provided, unspecified parameters will use config values
34
+ """
35
+ # Use config values for any unspecified parameters
36
+ if config is not None:
37
+ if model is None:
38
+ model = config.llm_model
39
+ if temperature is None:
40
+ temperature = config.temperature
41
+ if source is None:
42
+ source = config.source
43
+ if base_url is None:
44
+ base_url = config.base_url
45
+ if api_key is None:
46
+ api_key = config.api_key or "EMPTY"
47
+
48
+ # Use defaults if still not specified
49
+ if model is None:
50
+ model = "claude-3-5-sonnet-20241022"
51
+ if temperature is None:
52
+ temperature = 0.7
53
+ if api_key is None:
54
+ api_key = "EMPTY"
55
+ # Auto-detect source from model name if not specified
56
+ if source is None:
57
+ env_source = os.getenv("LLM_SOURCE")
58
+ if env_source in ALLOWED_SOURCES:
59
+ source = env_source
60
+ else:
61
+ if model[:7] == "claude-":
62
+ source = "Anthropic"
63
+ elif model[:7] == "gpt-oss":
64
+ source = "Ollama"
65
+ elif model[:4] == "gpt-":
66
+ source = "OpenAI"
67
+ elif model.startswith("azure-"):
68
+ source = "AzureOpenAI"
69
+ elif model[:7] == "gemini-":
70
+ source = "Gemini"
71
+ elif "groq" in model.lower():
72
+ source = "Groq"
73
+ elif base_url is not None:
74
+ source = "Custom"
75
+ elif "/" in model or any(
76
+ name in model.lower()
77
+ for name in [
78
+ "llama",
79
+ "mistral",
80
+ "qwen",
81
+ "gemma",
82
+ "phi",
83
+ "dolphin",
84
+ "orca",
85
+ "vicuna",
86
+ "deepseek",
87
+ ]
88
+ ):
89
+ source = "Ollama"
90
+ elif model.startswith(
91
+ ("anthropic.claude-", "amazon.titan-", "meta.llama-", "mistral.", "cohere.", "ai21.", "us.")
92
+ ):
93
+ source = "Bedrock"
94
+ else:
95
+ raise ValueError("Unable to determine model source. Please specify 'source' parameter.")
96
+
97
+ # Create appropriate model based on source
98
+ if source == "OpenAI":
99
+ try:
100
+ from langchain_openai import ChatOpenAI
101
+ except ImportError:
102
+ raise ImportError( # noqa: B904
103
+ "langchain-openai package is required for OpenAI models. Install with: pip install langchain-openai"
104
+ )
105
+ # Newer OpenAI models (e.g., gpt-5-*) require the Responses API and may reject
106
+ # legacy Chat Completions parameters like `stop`. Force Responses API when
107
+ # using gpt-5 models to avoid 400 errors such as: "Unsupported parameter: 'stop'".
108
+ use_responses = model.startswith("gpt-5")
109
+
110
+ if use_responses:
111
+ # Define a minimal subclass that drops the `stop` field when using the
112
+ # Responses API, since certain models (gpt-5-*) reject it entirely.
113
+ class _ChatOpenAIResponsesNoStop(ChatOpenAI):
114
+ def _get_request_payload(self, input_, *, stop=None, **kwargs): # type: ignore[override]
115
+ payload = super()._get_request_payload(input_, stop=stop, **kwargs)
116
+ try:
117
+ # If this call will use the Responses API, drop `stop` to avoid 400s.
118
+ if hasattr(self, "_use_responses_api") and self._use_responses_api(payload): # type: ignore[attr-defined]
119
+ payload.pop("stop", None)
120
+ # Also drop temperature for gpt-5 models as they only support default value
121
+ payload.pop("temperature", None)
122
+ except Exception:
123
+ # Be conservative: if anything goes wrong, still remove `stop` and `temperature`.
124
+ payload.pop("stop", None)
125
+ payload.pop("temperature", None)
126
+ return payload
127
+
128
+ return _ChatOpenAIResponsesNoStop(
129
+ model=model,
130
+ temperature=1, # Set to default value for gpt-5, will be removed in payload
131
+ stop_sequences=stop_sequences,
132
+ use_responses_api=True,
133
+ output_version="v0",
134
+ )
135
+ else:
136
+ return ChatOpenAI(
137
+ model=model,
138
+ temperature=temperature,
139
+ stop_sequences=stop_sequences,
140
+ )
141
+
142
+ elif source == "AzureOpenAI":
143
+ try:
144
+ from langchain_openai import AzureChatOpenAI
145
+ except ImportError:
146
+ raise ImportError( # noqa: B904
147
+ "langchain-openai package is required for Azure OpenAI models. Install with: pip install langchain-openai"
148
+ )
149
+ API_VERSION = "2024-12-01-preview"
150
+ model = model.replace("azure-", "")
151
+ return AzureChatOpenAI(
152
+ openai_api_key=os.getenv("OPENAI_API_KEY"),
153
+ azure_endpoint=os.getenv("OPENAI_ENDPOINT"),
154
+ azure_deployment=model,
155
+ openai_api_version=API_VERSION,
156
+ temperature=temperature,
157
+ )
158
+
159
+ elif source == "Anthropic":
160
+ try:
161
+ from langchain_anthropic import ChatAnthropic
162
+ except ImportError:
163
+ raise ImportError( # noqa: B904
164
+ "langchain-anthropic package is required for Anthropic models. Install with: pip install langchain-anthropic"
165
+ )
166
+
167
+ # Ensure ANTHROPIC_API_KEY is loaded from bash_profile if not in environment
168
+ if not os.environ.get("ANTHROPIC_API_KEY"):
169
+ try:
170
+ import subprocess
171
+
172
+ result = subprocess.run(
173
+ ["bash", "-c", "source ~/.bash_profile 2>/dev/null && echo $ANTHROPIC_API_KEY"],
174
+ capture_output=True,
175
+ text=True,
176
+ timeout=5,
177
+ )
178
+ if result.stdout.strip():
179
+ os.environ["ANTHROPIC_API_KEY"] = result.stdout.strip()
180
+ print("✓ Loaded ANTHROPIC_API_KEY from ~/.bash_profile")
181
+ except Exception as e:
182
+ print(f"Note: Could not load ANTHROPIC_API_KEY from bash_profile: {e}")
183
+
184
+ # Newer Claude models can reject an explicit temperature parameter.
185
+ # Omit it and use provider defaults to keep compatibility.
186
+ return ChatAnthropic(
187
+ model=model,
188
+ max_tokens=8192,
189
+ stop_sequences=stop_sequences,
190
+ )
191
+
192
+ elif source == "Gemini":
193
+ # If you want to use ChatGoogleGenerativeAI, you need to pass the stop sequences upon invoking the model.
194
+ # return ChatGoogleGenerativeAI(
195
+ # model=model,
196
+ # temperature=temperature,
197
+ # google_api_key=api_key,
198
+ # )
199
+ try:
200
+ from langchain_openai import ChatOpenAI
201
+ except ImportError:
202
+ raise ImportError( # noqa: B904
203
+ "langchain-openai package is required for Gemini models. Install with: pip install langchain-openai"
204
+ )
205
+ return ChatOpenAI(
206
+ model=model,
207
+ temperature=temperature,
208
+ api_key=os.getenv("GEMINI_API_KEY"),
209
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
210
+ stop_sequences=stop_sequences,
211
+ )
212
+
213
+ elif source == "Groq":
214
+ try:
215
+ from langchain_openai import ChatOpenAI
216
+ except ImportError:
217
+ raise ImportError( # noqa: B904
218
+ "langchain-openai package is required for Groq models. Install with: pip install langchain-openai"
219
+ )
220
+ return ChatOpenAI(
221
+ model=model,
222
+ temperature=temperature,
223
+ api_key=os.getenv("GROQ_API_KEY"),
224
+ base_url="https://api.groq.com/openai/v1",
225
+ stop_sequences=stop_sequences,
226
+ )
227
+
228
+ elif source == "Ollama":
229
+ try:
230
+ from langchain_ollama import ChatOllama
231
+ except ImportError:
232
+ raise ImportError( # noqa: B904
233
+ "langchain-ollama package is required for Ollama models. Install with: pip install langchain-ollama"
234
+ )
235
+ return ChatOllama(
236
+ model=model,
237
+ temperature=temperature,
238
+ )
239
+
240
+ elif source == "Bedrock":
241
+ try:
242
+ from langchain_aws import ChatBedrock
243
+ except ImportError:
244
+ raise ImportError( # noqa: B904
245
+ "langchain-aws package is required for Bedrock models. Install with: pip install langchain-aws"
246
+ )
247
+ return ChatBedrock(
248
+ model=model,
249
+ temperature=temperature,
250
+ stop_sequences=stop_sequences,
251
+ region_name=os.getenv("AWS_REGION", "us-east-1"),
252
+ )
253
+
254
+ elif source == "Custom":
255
+ try:
256
+ from langchain_openai import ChatOpenAI
257
+ except ImportError:
258
+ raise ImportError( # noqa: B904
259
+ "langchain-openai package is required for custom models. Install with: pip install langchain-openai"
260
+ )
261
+ # Custom LLM serving such as SGLang. Must expose an openai compatible API.
262
+ assert base_url is not None, "base_url must be provided for customly served LLMs"
263
+ llm = ChatOpenAI(
264
+ model=model,
265
+ temperature=temperature,
266
+ max_tokens=8192,
267
+ stop_sequences=stop_sequences,
268
+ base_url=base_url,
269
+ api_key=api_key,
270
+ )
271
+ return llm
272
+
273
+ else:
274
+ raise ValueError(
275
+ f"Invalid source: {source}. Valid options are 'OpenAI', 'AzureOpenAI', 'Anthropic', 'Gemini', 'Groq', 'Bedrock', or 'Ollama'"
276
+ )
Biomanus_upload/biomni/model/__init__.py ADDED
File without changes
Biomanus_upload/biomni/model/query_rewriter.py ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+
4
+ from langchain_core.messages import HumanMessage
5
+
6
+
7
+ class QueryRewriter:
8
+ """Rewrite vague user queries into a retrieval-friendly task description."""
9
+
10
+ STOPWORDS = {
11
+ "a",
12
+ "an",
13
+ "and",
14
+ "are",
15
+ "as",
16
+ "at",
17
+ "be",
18
+ "by",
19
+ "for",
20
+ "from",
21
+ "how",
22
+ "i",
23
+ "in",
24
+ "into",
25
+ "is",
26
+ "it",
27
+ "me",
28
+ "of",
29
+ "on",
30
+ "or",
31
+ "our",
32
+ "please",
33
+ "show",
34
+ "that",
35
+ "the",
36
+ "their",
37
+ "this",
38
+ "to",
39
+ "use",
40
+ "using",
41
+ "want",
42
+ "we",
43
+ "what",
44
+ "with",
45
+ }
46
+
47
+ CATEGORY_KEYWORDS = {
48
+ "database_question_answering": {
49
+ "according to",
50
+ "gene set",
51
+ "msigdb",
52
+ "ensembl",
53
+ "clinvar",
54
+ "uniprot",
55
+ "pubmed",
56
+ "gtrd",
57
+ "mirdb",
58
+ "database",
59
+ "located",
60
+ "chromosome",
61
+ "geneid",
62
+ },
63
+ "sequence_question_answering": {
64
+ "sequence",
65
+ "fasta",
66
+ "motif",
67
+ "binding site",
68
+ "primer",
69
+ "reverse complement",
70
+ "codon",
71
+ "peptide",
72
+ "protein sequence",
73
+ "dna sequence",
74
+ "rna sequence",
75
+ "promoter",
76
+ },
77
+ "transcriptomics": {
78
+ "rna",
79
+ "rna-seq",
80
+ "bulk",
81
+ "differential",
82
+ "expression",
83
+ "deseq2",
84
+ "counts",
85
+ },
86
+ "single_cell": {
87
+ "single-cell",
88
+ "singlecell",
89
+ "scrna",
90
+ "scanpy",
91
+ "seurat",
92
+ "cell",
93
+ "cluster",
94
+ },
95
+ "genomics": {
96
+ "genome",
97
+ "variant",
98
+ "vcf",
99
+ "alignment",
100
+ "bwa",
101
+ "gatk",
102
+ "wgs",
103
+ "dna",
104
+ },
105
+ "pathway_enrichment": {
106
+ "gsea",
107
+ "enrichment",
108
+ "pathway",
109
+ "kegg",
110
+ "fgsea",
111
+ "clusterprofiler",
112
+ },
113
+ "statistics": {
114
+ "statistics",
115
+ "test",
116
+ "pvalue",
117
+ "qvalue",
118
+ "regression",
119
+ "significance",
120
+ },
121
+ "literature": {
122
+ "paper",
123
+ "literature",
124
+ "pubmed",
125
+ "publication",
126
+ "article",
127
+ "citation",
128
+ },
129
+ "utility": {
130
+ "json",
131
+ "csv",
132
+ "table",
133
+ "parse",
134
+ "transform",
135
+ "filter",
136
+ "convert",
137
+ "merge",
138
+ },
139
+ }
140
+
141
+ def rewrite(self, query: str, llm=None) -> dict:
142
+ fallback = self._build_fallback(query)
143
+ if llm is None:
144
+ return fallback
145
+
146
+ prompt = f"""
147
+ You rewrite vague biomedical analysis requests into a retrieval-friendly task specification.
148
+
149
+ Return ONLY valid JSON with these keys:
150
+ - task_summary: short sentence
151
+ - rewritten_query: one concise retrieval-oriented query
152
+ - subtasks: list of concrete steps
153
+ - keywords: list of important terms
154
+ - server_hints: list of likely tool or server names if implied
155
+ - categories: list of analysis categories
156
+ - input_requirements: list of required data/input hints
157
+ - output_requirements: list of expected outputs
158
+ - constraints: list of runtime, environment, or quality constraints
159
+ - ambiguities: list of unresolved ambiguities
160
+
161
+ Rules:
162
+ - Preserve the user's intent.
163
+ - Expand vague requests into concrete analysis wording.
164
+ - Mention likely computational steps instead of conversational phrasing.
165
+ - Keep lists short and high-signal.
166
+
167
+ USER QUERY:
168
+ {query}
169
+ """
170
+
171
+ try:
172
+ response = llm.invoke([HumanMessage(content=prompt)])
173
+ content = self._extract_text(response.content)
174
+ parsed = self._parse_json(content)
175
+ if parsed:
176
+ merged = self._merge_with_fallback(fallback, parsed)
177
+ merged["retrieval_query"] = self._compose_retrieval_query(merged)
178
+ return merged
179
+ except Exception:
180
+ pass
181
+
182
+ return fallback
183
+
184
+ def _build_fallback(self, query: str) -> dict:
185
+ keywords = self._extract_keywords(query)
186
+ categories = self._infer_categories(keywords, query)
187
+ server_hints = self._infer_server_hints(keywords, query)
188
+ subtasks = self._infer_subtasks(query, categories)
189
+
190
+ payload = {
191
+ "original_query": query,
192
+ "task_summary": query.strip(),
193
+ "rewritten_query": query.strip(),
194
+ "subtasks": subtasks,
195
+ "keywords": keywords[:12],
196
+ "server_hints": server_hints[:10],
197
+ "categories": categories[:8],
198
+ "input_requirements": [],
199
+ "output_requirements": [],
200
+ "constraints": [],
201
+ "ambiguities": [],
202
+ }
203
+ payload["retrieval_query"] = self._compose_retrieval_query(payload)
204
+ return payload
205
+
206
+ def _merge_with_fallback(self, fallback: dict, parsed: dict) -> dict:
207
+ merged = dict(fallback)
208
+ for key in (
209
+ "task_summary",
210
+ "rewritten_query",
211
+ "subtasks",
212
+ "keywords",
213
+ "server_hints",
214
+ "categories",
215
+ "input_requirements",
216
+ "output_requirements",
217
+ "constraints",
218
+ "ambiguities",
219
+ ):
220
+ value = parsed.get(key)
221
+ if isinstance(value, list):
222
+ merged[key] = [str(item).strip() for item in value if str(item).strip()]
223
+ elif isinstance(value, str) and value.strip():
224
+ merged[key] = value.strip()
225
+
226
+ merged["keywords"] = self._merge_lists(merged.get("keywords", []), fallback.get("keywords", []), limit=14)
227
+ merged["server_hints"] = self._merge_lists(
228
+ merged.get("server_hints", []), fallback.get("server_hints", []), limit=10
229
+ )
230
+ merged["categories"] = self._merge_lists(
231
+ merged.get("categories", []), fallback.get("categories", []), limit=8
232
+ )
233
+ return merged
234
+
235
+ def _compose_retrieval_query(self, payload: dict) -> str:
236
+ parts = [
237
+ payload.get("rewritten_query", ""),
238
+ payload.get("task_summary", ""),
239
+ " ".join(payload.get("categories", [])),
240
+ " ".join(payload.get("keywords", [])),
241
+ " ".join(payload.get("server_hints", [])),
242
+ " ".join(payload.get("subtasks", [])),
243
+ ]
244
+ return " | ".join([part for part in parts if part]).strip()
245
+
246
+ def _extract_keywords(self, query: str) -> list[str]:
247
+ tokens = re.findall(r"[A-Za-z0-9_\-\.]{3,}", query.lower())
248
+ keywords = []
249
+ seen = set()
250
+ for token in tokens:
251
+ if token in self.STOPWORDS:
252
+ continue
253
+ if token not in seen:
254
+ seen.add(token)
255
+ keywords.append(token)
256
+ return keywords
257
+
258
+ def _infer_categories(self, keywords: list[str], query: str) -> list[str]:
259
+ text = f"{query.lower()} {' '.join(keywords)}"
260
+ categories = []
261
+ for category, hints in self.CATEGORY_KEYWORDS.items():
262
+ if any(hint in text for hint in hints):
263
+ categories.append(category)
264
+ db_hints = {"according to", "gene set", "msigdb", "ensembl", "clinvar", "uniprot", "mirdb", "gtrd", "omim", "disgenet"}
265
+ enrichment_hints = {"gsea", "enrichment", "pathway", "kegg", "fgsea", "clusterprofiler", "msigdb", "gene set"}
266
+ if "database_question_answering" in categories:
267
+ if "sequence_question_answering" in categories and not any(hint in text for hint in db_hints):
268
+ categories = [cat for cat in categories if cat != "database_question_answering"]
269
+ categories = [cat for cat in categories if cat not in {"transcriptomics", "single_cell"}]
270
+ if "sequence_question_answering" in categories:
271
+ if not any(hint in text for hint in enrichment_hints):
272
+ categories = [cat for cat in categories if cat != "pathway_enrichment"]
273
+ categories = [cat for cat in categories if cat != "single_cell"]
274
+ return categories or ["general_biomedical_analysis"]
275
+
276
+ def _infer_server_hints(self, keywords: list[str], query: str) -> list[str]:
277
+ text = f"{query.lower()} {' '.join(keywords)}"
278
+ hints = []
279
+ known = {
280
+ "deseq2": {"differential", "expression", "rna", "rna-seq", "counts", "deseq2"},
281
+ "fgsea": {"gsea", "fgsea", "enrichment", "ranked"},
282
+ "clusterprofiler": {"go", "kegg", "clusterprofiler", "pathway", "enrichment"},
283
+ "qvalue": {"qvalue", "fdr", "adjusted", "pvalue"},
284
+ "jq": {"json", "jq", "filter", "transform"},
285
+ "bwa": {"bwa", "alignment", "fastq", "genome"},
286
+ "gatk": {"gatk", "variant", "vcf", "germline", "somatic"},
287
+ "scvi": {"scvi", "single-cell", "singlecell", "latent", "integration"},
288
+ "bioconductor-ensembldb": {"ensembl", "chromosome", "geneid", "gene location", "transcript"},
289
+ "ensembl-vep": {"vep", "variant effect", "consequence", "clinvar", "variant annotation"},
290
+ "bioconductor-variantannotation": {"variant annotation", "dbsnp", "vcf", "rsid", "variant"},
291
+ "bioconductor-curatedatlasqueryr": {"cell atlas", "cell type", "tissue", "metadata query"},
292
+ "gseapy": {"gene set", "msigdb", "gsea", "enrichment"},
293
+ "bioconductor-clusterprofiler": {"gene set", "msigdb", "kegg", "go", "ontology"},
294
+ "gimmemotifs": {"motif", "binding site", "tfbs", "gtrd"},
295
+ "blast": {"blast", "homology", "alignment", "sequence"},
296
+ "primer3-py": {"primer", "tm", "amplicon"},
297
+ "pyfaidx": {"fasta", "sequence", "subsequence"},
298
+ "bedtools": {"bed", "interval", "genomic coordinate", "overlap"},
299
+ "samtools": {"samtools", "bam", "faidx", "fasta index"},
300
+ "ucsc-liftover": {"liftover", "assembly", "coordinate"},
301
+ "ucsc-twobittofa": {"2bit", "fasta", "sequence extraction"},
302
+ }
303
+ for name, patterns in known.items():
304
+ if any(pattern in text for pattern in patterns):
305
+ hints.append(name)
306
+ return hints
307
+
308
+ def _infer_subtasks(self, query: str, categories: list[str]) -> list[str]:
309
+ text = query.lower()
310
+ steps = []
311
+ if "database_question_answering" in categories:
312
+ steps.extend(
313
+ [
314
+ "identify the referenced biological database or knowledge source",
315
+ "retrieve the relevant annotation, gene set, or metadata entry",
316
+ "compare the answer choices against the retrieved record",
317
+ ]
318
+ )
319
+ if "sequence_question_answering" in categories:
320
+ steps.extend(
321
+ [
322
+ "identify the sequence operation implied by the question",
323
+ "retrieve or derive the required sequence or coordinate fragment",
324
+ "compare the answer choices against the computed sequence features",
325
+ ]
326
+ )
327
+ if "transcriptomics" in categories:
328
+ steps.extend(
329
+ [
330
+ "load count or expression matrix",
331
+ "run differential expression analysis",
332
+ ]
333
+ )
334
+ if "pathway_enrichment" in categories:
335
+ steps.extend(
336
+ [
337
+ "rank genes or features",
338
+ "run pathway or gene set enrichment",
339
+ ]
340
+ )
341
+ if "genomics" in categories:
342
+ steps.extend(
343
+ [
344
+ "prepare reads or aligned files",
345
+ "run alignment or variant calling workflow",
346
+ ]
347
+ )
348
+ if "single_cell" in categories:
349
+ steps.extend(
350
+ [
351
+ "load annotated single-cell object",
352
+ "perform clustering or annotation workflow",
353
+ ]
354
+ )
355
+ if "literature" in categories or "paper" in text:
356
+ steps.append("retrieve literature or metadata relevant to the study")
357
+ return self._merge_lists(steps, [], limit=8)
358
+
359
+ def _extract_text(self, response_content) -> str:
360
+ if isinstance(response_content, str):
361
+ return response_content
362
+ if isinstance(response_content, list):
363
+ parts = []
364
+ for item in response_content:
365
+ if isinstance(item, dict) and item.get("type") == "text":
366
+ parts.append(str(item.get("text", "")))
367
+ elif isinstance(item, str):
368
+ parts.append(item)
369
+ return "\n".join([part for part in parts if part])
370
+ return str(response_content)
371
+
372
+ def _parse_json(self, text: str) -> dict | None:
373
+ text = text.strip()
374
+ if not text:
375
+ return None
376
+ try:
377
+ return json.loads(text)
378
+ except json.JSONDecodeError:
379
+ match = re.search(r"\{[\s\S]*\}", text)
380
+ if not match:
381
+ return None
382
+ try:
383
+ return json.loads(match.group(0))
384
+ except json.JSONDecodeError:
385
+ return None
386
+
387
+ def _merge_lists(self, primary: list[str], secondary: list[str], limit: int) -> list[str]:
388
+ merged = []
389
+ seen = set()
390
+ for item in list(primary) + list(secondary):
391
+ normalized = str(item).strip()
392
+ lowered = normalized.lower()
393
+ if not normalized or lowered in seen:
394
+ continue
395
+ seen.add(lowered)
396
+ merged.append(normalized)
397
+ if len(merged) >= limit:
398
+ break
399
+ return merged
Biomanus_upload/biomni/model/retriever.py ADDED
@@ -0,0 +1,502 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextlib
2
+ import re
3
+
4
+ from langchain_core.messages import HumanMessage
5
+ from langchain_openai import ChatOpenAI
6
+
7
+
8
+ class ToolRetriever:
9
+ """Retrieve tools from the tool registry."""
10
+
11
+ SERVER_CATEGORY_HINTS = {
12
+ "transcriptomics": {
13
+ "deseq2",
14
+ "rna",
15
+ "rnaseq",
16
+ "bulk",
17
+ "expression",
18
+ "transcript",
19
+ },
20
+ "pathway_enrichment": {
21
+ "fgsea",
22
+ "gsea",
23
+ "clusterprofiler",
24
+ "pathway",
25
+ "go",
26
+ "kegg",
27
+ "reactome",
28
+ },
29
+ "single_cell": {
30
+ "singlecell",
31
+ "single-cell",
32
+ "scanpy",
33
+ "seurat",
34
+ "scvi",
35
+ "cell",
36
+ "neighborhood",
37
+ },
38
+ "genomics": {
39
+ "bwa",
40
+ "gatk",
41
+ "variant",
42
+ "genome",
43
+ "wgs",
44
+ "fastq",
45
+ "alignment",
46
+ },
47
+ "utility": {
48
+ "jq",
49
+ "json",
50
+ "csv",
51
+ "convert",
52
+ "parser",
53
+ "transform",
54
+ },
55
+ "literature": {
56
+ "pubmed",
57
+ "paper",
58
+ "literature",
59
+ "citation",
60
+ },
61
+ }
62
+
63
+ def __init__(self):
64
+ pass
65
+
66
+ def prompt_based_retrieval(self, query: str, resources: dict, llm=None) -> dict:
67
+ """Use a prompt-based approach to retrieve the most relevant resources for a query.
68
+
69
+ Args:
70
+ query: The user's query
71
+ resources: A dictionary with keys 'tools', 'data_lake', 'libraries', and 'know_how',
72
+ each containing a list of available resources
73
+ llm: Optional LLM instance to use for retrieval (if None, will create a new one)
74
+
75
+ Returns:
76
+ A dictionary with the same keys, but containing only the most relevant resources
77
+
78
+ """
79
+ # Build prompt sections for available resources
80
+ prompt_sections = []
81
+ prompt_sections.append(f"""
82
+ You are an expert biomedical research assistant. Your task is to select the relevant resources to help answer a user's query.
83
+
84
+ USER QUERY: {query}
85
+
86
+ Below are the available resources. For each category, select items that are directly or indirectly relevant to answering the query.
87
+ Be generous in your selection - include resources that might be useful for the task, even if they're not explicitly mentioned in the query.
88
+ It's better to include slightly more resources than to miss potentially useful ones.
89
+
90
+ AVAILABLE TOOLS:
91
+ {self._format_resources_for_prompt(resources.get("tools", []))}
92
+
93
+ AVAILABLE DATA LAKE ITEMS:
94
+ {self._format_resources_for_prompt(resources.get("data_lake", []))}
95
+
96
+ AVAILABLE SOFTWARE LIBRARIES:
97
+ {self._format_resources_for_prompt(resources.get("libraries", []))}""")
98
+
99
+ # Add know-how section if available
100
+ if "know_how" in resources and resources["know_how"]:
101
+ prompt_sections.append(f"""
102
+ AVAILABLE KNOW-HOW DOCUMENTS (Best Practices & Protocols):
103
+ {self._format_resources_for_prompt(resources.get("know_how", []))}""")
104
+
105
+ # Build response format based on available categories
106
+ response_format = """
107
+ For each category, respond with ONLY the indices of the relevant items in the following format:
108
+ TOOLS: [list of indices]
109
+ DATA_LAKE: [list of indices]
110
+ LIBRARIES: [list of indices]"""
111
+
112
+ if "know_how" in resources and resources["know_how"]:
113
+ response_format += "\nKNOW_HOW: [list of indices]"
114
+
115
+ response_format += """
116
+
117
+ For example:
118
+ TOOLS: [0, 3, 5, 7, 9]
119
+ DATA_LAKE: [1, 2, 4]
120
+ LIBRARIES: [0, 2, 4, 5, 8]"""
121
+
122
+ if "know_how" in resources and resources["know_how"]:
123
+ response_format += "\nKNOW_HOW: [0, 1]"
124
+
125
+ response_format += """
126
+
127
+ If a category has no relevant items, use an empty list, e.g., DATA_LAKE: []
128
+
129
+ IMPORTANT GUIDELINES:
130
+ 1. Be generous but not excessive - aim to include all potentially relevant resources
131
+ 2. ALWAYS prioritize database tools for general queries - include as many database tools as possible
132
+ 3. Include all literature search tools
133
+ 4. For wet lab sequence type of queries, ALWAYS include molecular biology tools
134
+ 5. For data lake items, include datasets that could provide useful information
135
+ 6. For libraries, include those that provide functions needed for analysis
136
+ 7. For know-how documents, include those that provide relevant protocols, best practices, or troubleshooting guidance
137
+ 8. Don't exclude resources just because they're not explicitly mentioned in the query
138
+ 9. When in doubt about a database tool or molecular biology tool, include it rather than exclude it
139
+ """
140
+
141
+ prompt = "\n".join(prompt_sections) + response_format
142
+
143
+ # Use the provided LLM or create a new one
144
+ if llm is None:
145
+ llm = ChatOpenAI(model="gpt-4o")
146
+
147
+ # Invoke the LLM
148
+ if hasattr(llm, "invoke"):
149
+ # For LangChain-style LLMs
150
+ response = llm.invoke([HumanMessage(content=prompt)])
151
+ response_content = response.content
152
+ else:
153
+ # For other LLM interfaces
154
+ response_content = str(llm(prompt))
155
+
156
+ # Parse the response to extract the selected indices
157
+ selected_indices = self._parse_llm_response(response_content)
158
+
159
+ # Get the selected resources
160
+ selected_resources = {
161
+ "tools": [
162
+ resources["tools"][i] for i in selected_indices.get("tools", []) if i < len(resources.get("tools", []))
163
+ ],
164
+ "data_lake": [
165
+ resources["data_lake"][i]
166
+ for i in selected_indices.get("data_lake", [])
167
+ if i < len(resources.get("data_lake", []))
168
+ ],
169
+ "libraries": [
170
+ resources["libraries"][i]
171
+ for i in selected_indices.get("libraries", [])
172
+ if i < len(resources.get("libraries", []))
173
+ ],
174
+ }
175
+
176
+ # Add know-how if present
177
+ if "know_how" in resources and resources["know_how"]:
178
+ selected_resources["know_how"] = [
179
+ resources["know_how"][i]
180
+ for i in selected_indices.get("know_how", [])
181
+ if i < len(resources.get("know_how", []))
182
+ ]
183
+
184
+ return selected_resources
185
+
186
+ def _format_resources_for_prompt(self, resources: list) -> str:
187
+ """Format resources for inclusion in the prompt."""
188
+ formatted = []
189
+ for i, resource in enumerate(resources):
190
+ if isinstance(resource, dict):
191
+ # Handle dictionary format (from tool registry or data lake/libraries with descriptions)
192
+ name = resource.get("name", f"Resource {i}")
193
+ description = resource.get("description", "")
194
+ formatted.append(f"{i}. {name}: {description}")
195
+ elif isinstance(resource, str):
196
+ # Handle string format (simple strings)
197
+ formatted.append(f"{i}. {resource}")
198
+ else:
199
+ # Try to extract name and description from tool objects
200
+ name = getattr(resource, "name", str(resource))
201
+ desc = getattr(resource, "description", "")
202
+ formatted.append(f"{i}. {name}: {desc}")
203
+
204
+ return "\n".join(formatted) if formatted else "None available"
205
+
206
+ def _parse_llm_response(self, response) -> dict:
207
+ """Parse the LLM response to extract the selected indices.
208
+
209
+ Accepts either a plain string or a Responses API-style list of content blocks.
210
+ """
211
+ # Normalize response to string if it's a list of content blocks (Responses API)
212
+ if isinstance(response, list):
213
+ parts = []
214
+ for item in response:
215
+ # LangChain Responses API returns list of dicts like {"type": "text", "text": "..."}
216
+ if isinstance(item, dict):
217
+ if item.get("type") == "text" and "text" in item:
218
+ parts.append(str(item.get("text", "")))
219
+ # If it's a tool_call or other block, ignore for this simple parsing
220
+ elif isinstance(item, str):
221
+ parts.append(item)
222
+ response = "\n".join([p for p in parts if p])
223
+ elif not isinstance(response, str):
224
+ response = str(response)
225
+ selected_indices = {"tools": [], "data_lake": [], "libraries": [], "know_how": []}
226
+
227
+ # Extract indices for each category
228
+ tools_match = re.search(r"TOOLS:\s*\[(.*?)\]", response, re.IGNORECASE)
229
+ if tools_match and tools_match.group(1).strip():
230
+ with contextlib.suppress(ValueError):
231
+ selected_indices["tools"] = [int(idx.strip()) for idx in tools_match.group(1).split(",") if idx.strip()]
232
+
233
+ data_lake_match = re.search(r"DATA_LAKE:\s*\[(.*?)\]", response, re.IGNORECASE)
234
+ if data_lake_match and data_lake_match.group(1).strip():
235
+ with contextlib.suppress(ValueError):
236
+ selected_indices["data_lake"] = [
237
+ int(idx.strip()) for idx in data_lake_match.group(1).split(",") if idx.strip()
238
+ ]
239
+
240
+ libraries_match = re.search(r"LIBRARIES:\s*\[(.*?)\]", response, re.IGNORECASE)
241
+ if libraries_match and libraries_match.group(1).strip():
242
+ with contextlib.suppress(ValueError):
243
+ selected_indices["libraries"] = [
244
+ int(idx.strip()) for idx in libraries_match.group(1).split(",") if idx.strip()
245
+ ]
246
+
247
+ # Extract know-how indices
248
+ know_how_match = re.search(r"KNOW[-_]HOW:\s*\[(.*?)\]", response, re.IGNORECASE)
249
+ if know_how_match and know_how_match.group(1).strip():
250
+ with contextlib.suppress(ValueError):
251
+ selected_indices["know_how"] = [
252
+ int(idx.strip()) for idx in know_how_match.group(1).split(",") if idx.strip()
253
+ ]
254
+
255
+ return selected_indices
256
+
257
+ def build_mcp_server_entry(self, server_name: str, safe_server_name: str, tools: list, server_meta: dict) -> dict:
258
+ """Build a retrieval document for one MCP server."""
259
+ normalized_tools = []
260
+ for tool in tools:
261
+ if not isinstance(tool, dict):
262
+ continue
263
+ normalized_tools.append(
264
+ {
265
+ "name": tool.get("name") or tool.get("biomni_name") or "unknown_tool",
266
+ "description": tool.get("description", ""),
267
+ "inputSchema": tool.get(
268
+ "inputSchema",
269
+ {"properties": tool.get("parameters", {}), "required": []},
270
+ ),
271
+ }
272
+ )
273
+
274
+ combined_text = " ".join(
275
+ [
276
+ server_name,
277
+ safe_server_name,
278
+ server_meta.get("description", ""),
279
+ " ".join(tool.get("name", "") for tool in normalized_tools),
280
+ " ".join(tool.get("description", "") for tool in normalized_tools),
281
+ ]
282
+ )
283
+ tokens = self._tokenize(combined_text)
284
+ category = self._infer_server_category(combined_text, tokens)
285
+ summary = (
286
+ server_meta.get("description")
287
+ or f"MCP server {server_name} with tools: "
288
+ + ", ".join(tool.get("name", "") for tool in normalized_tools[:8])
289
+ ).strip()
290
+
291
+ return {
292
+ "name": server_name,
293
+ "safe_name": safe_server_name,
294
+ "module": f"mcp_servers.{safe_server_name}",
295
+ "category": category,
296
+ "summary": summary,
297
+ "keywords": list(tokens)[:40],
298
+ "tools": normalized_tools,
299
+ "tool_names": [tool["name"] for tool in normalized_tools],
300
+ "server_meta": server_meta,
301
+ "score": 0.0,
302
+ }
303
+
304
+ def hybrid_server_retrieval(
305
+ self,
306
+ query_context: dict,
307
+ server_entries: list[dict],
308
+ llm=None,
309
+ top_k: int = 8,
310
+ candidate_pool: int = 24,
311
+ ) -> list[dict]:
312
+ """Select relevant MCP servers with lexical scoring plus optional LLM reranking."""
313
+ if not server_entries:
314
+ return []
315
+
316
+ scored = []
317
+ for idx, entry in enumerate(server_entries):
318
+ score = self._score_server_entry(query_context, entry)
319
+ enriched = dict(entry)
320
+ enriched["score"] = score
321
+ scored.append((score, idx, enriched))
322
+
323
+ scored.sort(key=lambda item: item[0], reverse=True)
324
+ shortlisted = [entry for _, _, entry in scored[: max(top_k, candidate_pool)]]
325
+
326
+ selected = self._llm_rerank_servers(query_context, shortlisted, top_k=top_k, llm=llm)
327
+ if selected:
328
+ return selected
329
+ return shortlisted[:top_k]
330
+
331
+ def hybrid_tool_selection(
332
+ self,
333
+ query_context: dict,
334
+ tools: list[dict],
335
+ llm=None,
336
+ top_k: int = 12,
337
+ ) -> list[dict]:
338
+ """Select high-signal tools inside a shortlisted MCP server."""
339
+ if len(tools) <= top_k:
340
+ return tools
341
+
342
+ scored = []
343
+ for idx, tool in enumerate(tools):
344
+ score = self._score_tool_entry(query_context, tool)
345
+ scored.append((score, idx, tool))
346
+ scored.sort(key=lambda item: item[0], reverse=True)
347
+ shortlisted = [tool for _, _, tool in scored[: max(top_k * 2, top_k)]]
348
+
349
+ selected = self._llm_rerank_tools(query_context, shortlisted, top_k=top_k, llm=llm)
350
+ if selected:
351
+ return selected
352
+ return shortlisted[:top_k]
353
+
354
+ def _score_server_entry(self, query_context: dict, entry: dict) -> float:
355
+ query_tokens = self._tokenize(self._query_text(query_context))
356
+ if not query_tokens:
357
+ return 0.0
358
+
359
+ server_tokens = set(entry.get("keywords", [])) | self._tokenize(entry.get("summary", ""))
360
+ overlap = len(query_tokens & server_tokens)
361
+ score = float(overlap)
362
+
363
+ server_name = entry.get("name", "").lower()
364
+ category = entry.get("category", "").lower()
365
+
366
+ for hint in query_context.get("server_hints", []):
367
+ hint_l = str(hint).lower()
368
+ if hint_l and hint_l in server_name:
369
+ score += 5.0
370
+
371
+ for category_hint in query_context.get("categories", []):
372
+ if str(category_hint).lower() == category:
373
+ score += 3.0
374
+
375
+ tool_names = " ".join(entry.get("tool_names", [])).lower()
376
+ for token in query_tokens:
377
+ if token in server_name:
378
+ score += 2.5
379
+ if token in tool_names:
380
+ score += 1.0
381
+
382
+ return score
383
+
384
+ def _score_tool_entry(self, query_context: dict, tool: dict) -> float:
385
+ query_tokens = self._tokenize(self._query_text(query_context))
386
+ tool_text = " ".join([tool.get("name", ""), tool.get("description", "")])
387
+ tool_tokens = self._tokenize(tool_text)
388
+ overlap = len(query_tokens & tool_tokens)
389
+ score = float(overlap)
390
+
391
+ for token in query_tokens:
392
+ if token in tool.get("name", "").lower():
393
+ score += 2.0
394
+
395
+ for hint in query_context.get("server_hints", []):
396
+ hint_l = str(hint).lower()
397
+ if hint_l and hint_l in tool_text.lower():
398
+ score += 2.5
399
+
400
+ return score
401
+
402
+ def _llm_rerank_servers(self, query_context: dict, candidates: list[dict], top_k: int, llm=None) -> list[dict]:
403
+ if llm is None or not candidates:
404
+ return []
405
+
406
+ lines = []
407
+ for idx, candidate in enumerate(candidates):
408
+ lines.append(
409
+ f"{idx}. {candidate['name']} | category={candidate.get('category', 'unknown')} | "
410
+ f"tools={', '.join(candidate.get('tool_names', [])[:8])} | summary={candidate.get('summary', '')}"
411
+ )
412
+
413
+ prompt = f"""
414
+ Select the most relevant MCP servers for this query.
415
+
416
+ QUERY:
417
+ {self._query_text(query_context)}
418
+
419
+ SERVER CANDIDATES:
420
+ {chr(10).join(lines)}
421
+
422
+ Return ONLY:
423
+ SERVERS: [indices]
424
+
425
+ Choose at most {top_k} servers.
426
+ Prefer precise servers over broad but irrelevant ones.
427
+ """
428
+ try:
429
+ response = llm.invoke([HumanMessage(content=prompt)])
430
+ content = response.content if hasattr(response, "content") else str(response)
431
+ text = self._normalize_text(content)
432
+ match = re.search(r"SERVERS:\s*\[(.*?)\]", text, re.IGNORECASE)
433
+ if not match:
434
+ return []
435
+ indices = [int(idx.strip()) for idx in match.group(1).split(",") if idx.strip()]
436
+ return [candidates[idx] for idx in indices if 0 <= idx < len(candidates)]
437
+ except Exception:
438
+ return []
439
+
440
+ def _llm_rerank_tools(self, query_context: dict, candidates: list[dict], top_k: int, llm=None) -> list[dict]:
441
+ if llm is None or not candidates:
442
+ return []
443
+
444
+ lines = []
445
+ for idx, tool in enumerate(candidates):
446
+ lines.append(f"{idx}. {tool.get('name', 'unknown')}: {tool.get('description', '')}")
447
+
448
+ prompt = f"""
449
+ Select the most relevant MCP tools for this query.
450
+
451
+ QUERY:
452
+ {self._query_text(query_context)}
453
+
454
+ TOOL CANDIDATES:
455
+ {chr(10).join(lines)}
456
+
457
+ Return ONLY:
458
+ TOOLS: [indices]
459
+
460
+ Choose at most {top_k} tools.
461
+ """
462
+ try:
463
+ response = llm.invoke([HumanMessage(content=prompt)])
464
+ content = response.content if hasattr(response, "content") else str(response)
465
+ text = self._normalize_text(content)
466
+ match = re.search(r"TOOLS:\s*\[(.*?)\]", text, re.IGNORECASE)
467
+ if not match:
468
+ return []
469
+ indices = [int(idx.strip()) for idx in match.group(1).split(",") if idx.strip()]
470
+ return [candidates[idx] for idx in indices if 0 <= idx < len(candidates)]
471
+ except Exception:
472
+ return []
473
+
474
+ def _infer_server_category(self, text: str, tokens: set[str]) -> str:
475
+ lowered = text.lower()
476
+ for category, hints in self.SERVER_CATEGORY_HINTS.items():
477
+ if any(hint in lowered for hint in hints) or tokens.intersection(hints):
478
+ return category
479
+ return "general"
480
+
481
+ def _query_text(self, query_context: dict) -> str:
482
+ if not isinstance(query_context, dict):
483
+ return str(query_context)
484
+ return query_context.get("retrieval_query") or query_context.get("rewritten_query") or query_context.get(
485
+ "original_query", ""
486
+ )
487
+
488
+ def _tokenize(self, text: str) -> set[str]:
489
+ return {token for token in re.findall(r"[a-zA-Z0-9_\-\.]{3,}", str(text).lower())}
490
+
491
+ def _normalize_text(self, content) -> str:
492
+ if isinstance(content, str):
493
+ return content
494
+ if isinstance(content, list):
495
+ parts = []
496
+ for item in content:
497
+ if isinstance(item, dict) and item.get("type") == "text":
498
+ parts.append(str(item.get("text", "")))
499
+ elif isinstance(item, str):
500
+ parts.append(item)
501
+ return "\n".join([part for part in parts if part])
502
+ return str(content)
Biomanus_upload/biomni/task/__init__.py ADDED
File without changes
Biomanus_upload/biomni/task/base_task.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class base_task:
2
+ def __init__(self):
3
+ pass
4
+
5
+ def get_example(self):
6
+ pass
7
+
8
+ def get_iterator(self):
9
+ pass
10
+
11
+ def evaluate(self):
12
+ pass
13
+
14
+ def output_class(self):
15
+ pass
16
+
17
+ def get_prompt_from_input(self, input):
18
+ return self.get_example(input)["prompt"]
Biomanus_upload/biomni/task/hle.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from biomni.task.base_task import base_task
5
+
6
+ np.random.seed(42)
7
+
8
+
9
+ def shuffle(x):
10
+ np.random.shuffle(x)
11
+ return x
12
+
13
+
14
+ class humanity_last_exam(base_task):
15
+ def __init__(self, path="./data", category="Biology/Medicine", answer_type="multipleChoice"):
16
+ if category not in [
17
+ "Other",
18
+ "Humanities/Social Science",
19
+ "Math",
20
+ "Physics",
21
+ "Computer Science/AI",
22
+ "Biology/Medicine",
23
+ "Chemistry",
24
+ "Engineering",
25
+ ]:
26
+ raise ValueError(
27
+ "category must be one of ['Other', 'Humanities/Social Science', 'Math', 'Physics', 'Computer Science/AI', 'Biology/Medicine', 'Chemistry', 'Engineering']"
28
+ )
29
+ if answer_type not in ["exactMatch", "multipleChoice"]:
30
+ raise ValueError("answer_type must be one of ['exactMatch' or 'multipleChoice']")
31
+
32
+ self.dataset = category # Store dataset type
33
+ self.answer_type = answer_type
34
+ df = pd.read_parquet(path + "/hle/test_sampled_biology_medicine.parquet")
35
+
36
+ # Extract answer choices from question text
37
+ def extract_options(question):
38
+ # Find the "Answer Choices:" section
39
+ if "Answer Choices:" not in question:
40
+ return []
41
+ choices = question.split("Answer Choices:")[1].strip()
42
+
43
+ # Split on A., B., C. etc and clean up
44
+ options = []
45
+ letters = [
46
+ "A.",
47
+ "B.",
48
+ "C.",
49
+ "D.",
50
+ "E.",
51
+ "F.",
52
+ "G.",
53
+ "H.",
54
+ "I.",
55
+ "J.",
56
+ "K.",
57
+ "L.",
58
+ "M.",
59
+ "N.",
60
+ "O.",
61
+ "P.",
62
+ "Q.",
63
+ "R.",
64
+ "S.",
65
+ "T.",
66
+ "U.",
67
+ "V.",
68
+ "W.",
69
+ "X.",
70
+ "Y.",
71
+ "Z.",
72
+ ]
73
+ for i, letter in enumerate(letters):
74
+ if letter in choices:
75
+ # Define the next letter if available
76
+ next_letter = letters[i + 1] if i + 1 < len(letters) else None
77
+
78
+ # Split between current letter and next letter
79
+ parts = choices.split(letter)[1]
80
+ if next_letter and next_letter in parts:
81
+ option = parts.split(next_letter)[0].strip()
82
+ else:
83
+ option = parts.strip()
84
+
85
+ options.append(option)
86
+ return options
87
+
88
+ def extract_question(question):
89
+ return question.split("Answer Choices:")[0].strip()
90
+
91
+ # Extract options and answers only for multiple choice questions and category is the same as the dataset
92
+ df = df[df["category"] == self.dataset]
93
+ df = df[df["answer_type"] == "multipleChoice"]
94
+ df["question_text"] = df.question
95
+ df["letter_answer"] = df["answer"].apply(lambda x: x[0])
96
+
97
+ self.query = df.question_text.values
98
+ # self.options = df.options_letters.values
99
+ self.answer = df.letter_answer.values
100
+
101
+ self.prompt = """Question: {question}"""
102
+
103
+ def get_example(self, index=None):
104
+ if index is None:
105
+ index = np.random.randint(len(self.query))
106
+
107
+ return {
108
+ "prompt": self.prompt.format(
109
+ question=self.query[index],
110
+ # options = self.options[index]
111
+ ),
112
+ "answer": self.answer[index],
113
+ }
114
+
115
+ def get_iterator(self):
116
+ for i in range(len(self.query)):
117
+ yield self.get_example(i)
118
+
119
+ def evaluate(self, response):
120
+ ## expected a list/array of symbols
121
+ from sklearn.metrics import accuracy_score
122
+
123
+ ground_truth = self.answer
124
+ response = np.array(response)
125
+
126
+ return {
127
+ "accuracy": accuracy_score(ground_truth, response),
128
+ "coverage": np.mean(response != self.refrain_label),
129
+ "refrain_ratio": np.mean(response == self.refrain_label),
130
+ "precision": accuracy_score(
131
+ ground_truth[np.where(response != self.refrain_label)],
132
+ response[np.where(response != self.refrain_label)],
133
+ ),
134
+ }
135
+
136
+ def output_class(self):
137
+ from pydantic import BaseModel, Field
138
+
139
+ class MultipleChoiceOutput(BaseModel):
140
+ """Multiple choice output."""
141
+
142
+ choice: str | None = Field(
143
+ description="Multiple choice answer. For example, if there is <answer>A</answer> in the prompt, the output should be 'A'."
144
+ )
145
+
146
+ return MultipleChoiceOutput
Biomanus_upload/biomni/task/lab_bench.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from biomni.task.base_task import base_task
5
+
6
+ np.random.seed(42)
7
+
8
+
9
+ def shuffle(x):
10
+ np.random.shuffle(x)
11
+ return x
12
+
13
+
14
+ class lab_bench(base_task):
15
+ def __init__(self, path="./data", dataset="DbQA"):
16
+ if dataset not in ["DbQA", "SeqQA"]:
17
+ raise ValueError("dataset must be one of 'DbQA', 'SeqQA'")
18
+
19
+ self.dataset = dataset # Store dataset type
20
+ df = pd.read_parquet(path + "/" + dataset + "/train-00000-of-00001_test.parquet")
21
+
22
+ self.prompt = """The following is a multiple choice question about biology.
23
+ Please answer by responding with the letter of the correct answer.
24
+
25
+ Question: {question}
26
+ Options:
27
+ {options}
28
+
29
+ You MUST include the letter of the correct answer within the following tags:
30
+ [ANSWER] and [/ANSWER]. For example, '[ANSWER]<answer>[/ANSWER]',
31
+ where <answer> is the correct letter. Always answer in exactly this format
32
+ of a single letter between the two tags, even if you are unsure.
33
+ We require this because we use automatic parsing.
34
+ """
35
+
36
+ np.random.seed(42)
37
+ df["options"] = df.apply(
38
+ lambda x: shuffle(
39
+ x.distractors.tolist() + [x.ideal] + ["Insufficient information to answer the question."]
40
+ ),
41
+ axis=1,
42
+ )
43
+ df["options_letters"] = df.options.apply(
44
+ lambda x: "\n".join([chr(ord("A") + i) + "." + item for i, item in enumerate(x)])
45
+ )
46
+ df["letter_answer"] = df.apply(
47
+ lambda x: chr(ord("A") + np.where(np.array(x.options) == x.ideal)[0][0]),
48
+ axis=1,
49
+ )
50
+ df["letter_refrain"] = df.apply(
51
+ lambda x: chr(
52
+ ord("A") + np.where(np.array(x.options) == "Insufficient information to answer the question.")[0][0]
53
+ ),
54
+ axis=1,
55
+ )
56
+
57
+ self.query = df.question.values
58
+ self.options = df.options_letters.values
59
+ self.answer = df.letter_answer.values
60
+ self.refrain_label = df.letter_refrain.values
61
+
62
+ # Store protocol information if available
63
+ self.protocol = df.protocol.values if "protocol" in df.columns else None
64
+
65
+ def get_example(self, index=None):
66
+ if index is None:
67
+ index = np.random.randint(len(self.query))
68
+
69
+ if self.dataset == "ProtocolQA" and self.protocol is not None:
70
+ return {
71
+ "prompt": self.prompt.format(
72
+ protocol=self.protocol[index],
73
+ question=self.query[index],
74
+ options=self.options[index],
75
+ ),
76
+ "answer": self.answer[index],
77
+ }
78
+ else:
79
+ return {
80
+ "prompt": self.prompt.format(question=self.query[index], options=self.options[index]),
81
+ "answer": self.answer[index],
82
+ }
83
+
84
+ def get_iterator(self):
85
+ for i in range(len(self.query)):
86
+ yield self.get_example(i)
87
+
88
+ def evaluate(self, response):
89
+ ## expected a list/array of symbols
90
+ from sklearn.metrics import accuracy_score
91
+
92
+ ground_truth = self.answer
93
+ response = np.array(response)
94
+
95
+ return {
96
+ "accuracy": accuracy_score(ground_truth, response),
97
+ "coverage": np.mean(response != self.refrain_label),
98
+ "refrain_ratio": np.mean(response == self.refrain_label),
99
+ "precision": accuracy_score(
100
+ ground_truth[np.where(response != self.refrain_label)],
101
+ response[np.where(response != self.refrain_label)],
102
+ ),
103
+ }
104
+
105
+ def output_class(self):
106
+ from pydantic import BaseModel, Field
107
+
108
+ class MultipleChoiceOutput(BaseModel):
109
+ """Multiple choice output."""
110
+
111
+ choice: str | None = Field(
112
+ description="Multiple choice answer. For example, if there is <answer>A</answer> in the prompt, the output should be 'A'."
113
+ )
114
+
115
+ return MultipleChoiceOutput
Biomanus_upload/biomni/tool/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ try:
2
+ from biomni.utils import get_tool_decorated_functions # noqa: F401
3
+ except ModuleNotFoundError: # pragma: no cover
4
+ get_tool_decorated_functions = None # type: ignore
Biomanus_upload/biomni/tool/biochemistry.py ADDED
@@ -0,0 +1,1026 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def analyze_circular_dichroism_spectra(
2
+ sample_name,
3
+ sample_type,
4
+ wavelength_data,
5
+ cd_signal_data,
6
+ temperature_data=None,
7
+ thermal_cd_data=None,
8
+ output_dir="./",
9
+ ):
10
+ """Analyzes circular dichroism (CD) spectroscopy data to determine secondary structure and thermal stability.
11
+
12
+ Parameters
13
+ ----------
14
+ sample_name : str
15
+ Name of the biomolecule sample (e.g., "Znf706", "G-quadruplex")
16
+ sample_type : str
17
+ Type of biomolecule ("protein" or "nucleic_acid")
18
+ wavelength_data : list or numpy.ndarray
19
+ Wavelength values in nm for CD spectrum
20
+ cd_signal_data : list or numpy.ndarray
21
+ CD signal intensity values (typically in mdeg or Δε)
22
+ temperature_data : list or numpy.ndarray, optional
23
+ Temperature values (°C) for thermal denaturation experiment
24
+ thermal_cd_data : list or numpy.ndarray, optional
25
+ CD signal values at specific wavelength across different temperatures
26
+ output_dir : str, optional
27
+ Directory to save result files, defaults to current directory
28
+
29
+ Returns
30
+ -------
31
+ str
32
+ Research log summarizing the CD analysis steps and results
33
+
34
+ """
35
+ import os
36
+ from datetime import datetime
37
+
38
+ import numpy as np
39
+
40
+ # Initialize research log
41
+ log = f"# Circular Dichroism Analysis Report for {sample_name}\n"
42
+ log += f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
43
+ log += "## Sample Information\n"
44
+ log += f"- Sample Name: {sample_name}\n"
45
+ log += f"- Sample Type: {sample_type}\n\n"
46
+
47
+ # Convert inputs to numpy arrays if they aren't already
48
+ wavelength_data = np.array(wavelength_data)
49
+ cd_signal_data = np.array(cd_signal_data)
50
+
51
+ # Ensure output directory exists
52
+ if not os.path.exists(output_dir):
53
+ os.makedirs(output_dir)
54
+
55
+ # 1. Analyze CD spectrum for secondary structure
56
+ log += "## Secondary Structure Analysis\n"
57
+
58
+ # Different analysis approaches based on sample type
59
+ if sample_type.lower() == "protein":
60
+ # Analyze protein secondary structure based on characteristic spectral features
61
+ alpha_helix_signal = np.sum((wavelength_data >= 190) & (wavelength_data <= 195) & (cd_signal_data > 0))
62
+ beta_sheet_signal = np.sum((wavelength_data >= 215) & (wavelength_data <= 220) & (cd_signal_data < 0))
63
+ random_coil_signal = np.sum((wavelength_data >= 195) & (wavelength_data <= 200) & (cd_signal_data < 0))
64
+
65
+ # Simple classification based on signal patterns
66
+ if alpha_helix_signal > beta_sheet_signal and alpha_helix_signal > random_coil_signal:
67
+ structure = "predominantly alpha-helical"
68
+ elif beta_sheet_signal > alpha_helix_signal and beta_sheet_signal > random_coil_signal:
69
+ structure = "predominantly beta-sheet"
70
+ else:
71
+ structure = "mixed or predominantly random coil"
72
+
73
+ log += f"- The CD spectrum indicates {structure} structure for {sample_name}.\n"
74
+ log += "- Key spectral features:\n"
75
+ log += " - 190-195 nm region: associated with alpha-helical content\n"
76
+ log += " - 215-220 nm region: associated with beta-sheet content\n\n"
77
+
78
+ elif sample_type.lower() == "nucleic_acid":
79
+ # Analyze nucleic acid structure (e.g., G-quadruplex has characteristic positive peak ~295 nm)
80
+ g_quadruplex_signal = np.sum((wavelength_data >= 290) & (wavelength_data <= 300) & (cd_signal_data > 0))
81
+ b_form_signal = np.sum((wavelength_data >= 270) & (wavelength_data <= 280) & (cd_signal_data > 0))
82
+
83
+ if g_quadruplex_signal > 0:
84
+ structure = "G-quadruplex characteristics"
85
+ elif b_form_signal > 0:
86
+ structure = "B-form characteristics"
87
+ else:
88
+ structure = "non-standard structure"
89
+
90
+ log += f"- The CD spectrum indicates {structure} for {sample_name}.\n"
91
+ log += "- Key spectral features:\n"
92
+ log += " - 290-300 nm positive peak: characteristic of G-quadruplex structures\n"
93
+ log += " - 270-280 nm positive peak: characteristic of B-form DNA\n\n"
94
+
95
+ # Save spectral data results
96
+ spectral_file = os.path.join(output_dir, f"{sample_name}_cd_spectrum_analysis.txt")
97
+ with open(spectral_file, "w") as f:
98
+ f.write("Wavelength (nm)\tCD Signal\n")
99
+ for wl, signal in zip(wavelength_data, cd_signal_data, strict=False):
100
+ f.write(f"{wl:.1f}\t{signal:.4f}\n")
101
+
102
+ log += f"- Detailed spectral data saved to: {spectral_file}\n\n"
103
+
104
+ # 2. Thermal stability analysis (if temperature data provided)
105
+ if temperature_data is not None and thermal_cd_data is not None:
106
+ temperature_data = np.array(temperature_data)
107
+ thermal_cd_data = np.array(thermal_cd_data)
108
+
109
+ log += "## Thermal Stability Analysis\n"
110
+
111
+ # Simple Tm estimation (melting temperature) - find temperature at 50% unfolding
112
+ # Normalize thermal data to 0-1 range for unfolding fraction
113
+ min_signal = np.min(thermal_cd_data)
114
+ max_signal = np.max(thermal_cd_data)
115
+ unfolded_fraction = (thermal_cd_data - min_signal) / (max_signal - min_signal)
116
+
117
+ # Find the temperature closest to 50% unfolding
118
+ tm_idx = np.argmin(np.abs(unfolded_fraction - 0.5))
119
+ tm = temperature_data[tm_idx]
120
+
121
+ log += f"- Estimated melting temperature (Tm): {tm:.1f}°C\n"
122
+
123
+ # Cooperativity assessment (crude estimate based on transition steepness)
124
+ t_range = temperature_data[-1] - temperature_data[0]
125
+ transition_width = (
126
+ t_range / len(temperature_data) * np.sum((unfolded_fraction > 0.2) & (unfolded_fraction < 0.8))
127
+ )
128
+
129
+ if transition_width < 0.2 * t_range:
130
+ cooperativity = "highly cooperative (sharp transition)"
131
+ elif transition_width < 0.4 * t_range:
132
+ cooperativity = "moderately cooperative"
133
+ else:
134
+ cooperativity = "non-cooperative (broad transition)"
135
+
136
+ log += f"- Thermal transition: {cooperativity}\n"
137
+
138
+ # Save thermal denaturation data
139
+ thermal_file = os.path.join(output_dir, f"{sample_name}_thermal_denaturation.txt")
140
+ with open(thermal_file, "w") as f:
141
+ f.write("Temperature (°C)\tCD Signal\tUnfolded Fraction\n")
142
+ for temp, signal, unfold in zip(temperature_data, thermal_cd_data, unfolded_fraction, strict=False):
143
+ f.write(f"{temp:.1f}\t{signal:.4f}\t{unfold:.4f}\n")
144
+
145
+ log += f"- Thermal denaturation data saved to: {thermal_file}\n\n"
146
+
147
+ # 3. Summary and conclusions
148
+ log += "## Conclusions\n"
149
+ if sample_type.lower() == "protein":
150
+ log += f"- {sample_name} shows {structure} according to CD spectroscopy.\n"
151
+ else:
152
+ log += f"- {sample_name} exhibits {structure} according to CD spectroscopy.\n"
153
+
154
+ if temperature_data is not None:
155
+ log += f"- The molecule has a melting temperature of {tm:.1f}°C with {cooperativity}.\n"
156
+
157
+ return log
158
+
159
+
160
+ def analyze_rna_secondary_structure_features(dot_bracket_structure, sequence=None):
161
+ """Calculate numeric values for various structural features of an RNA secondary structure.
162
+
163
+ Parameters
164
+ ----------
165
+ dot_bracket_structure : str
166
+ RNA secondary structure in dot-bracket notation (e.g., "(((...)))").
167
+ Parentheses represent base pairs, dots represent unpaired bases.
168
+ sequence : str, optional
169
+ The RNA sequence corresponding to the structure. If provided,
170
+ sequence-dependent energy calculations will be performed.
171
+
172
+ Returns
173
+ -------
174
+ str
175
+ A research log summarizing the calculated structural features and analysis steps.
176
+
177
+ """
178
+ # Initialize research log
179
+ log = "# RNA Secondary Structure Feature Analysis\n\n"
180
+
181
+ # Validate input
182
+ if not all(c in "().[]{}" for c in dot_bracket_structure):
183
+ return "Error: Invalid dot-bracket notation. Use only '()', '[]', '{}', and '.'"
184
+
185
+ log += f"Input structure (length: {len(dot_bracket_structure)}): {dot_bracket_structure}\n"
186
+ if sequence:
187
+ log += f"Input sequence (length: {len(sequence)}): {sequence}\n"
188
+ if len(sequence) != len(dot_bracket_structure):
189
+ return "Error: Sequence and structure lengths do not match."
190
+
191
+ # Extract base pairs
192
+ pairs = []
193
+ stack = []
194
+
195
+ for i, char in enumerate(dot_bracket_structure):
196
+ if char in "([{":
197
+ stack.append((i, char))
198
+ elif char in ")]}":
199
+ if not stack:
200
+ return "Error: Unbalanced structure. More closing than opening brackets."
201
+
202
+ j, opening_char = stack.pop()
203
+ # Check for matching bracket types
204
+ if (
205
+ (opening_char == "(" and char != ")")
206
+ or (opening_char == "[" and char != "]")
207
+ or (opening_char == "{" and char != "}")
208
+ ):
209
+ return "Error: Mismatched bracket types. Opening and closing brackets must match."
210
+
211
+ pairs.append((j, i))
212
+
213
+ if stack:
214
+ return "Error: Unbalanced structure. More opening than closing brackets."
215
+
216
+ # Sort pairs by position
217
+ pairs.sort()
218
+
219
+ # Identify stems (consecutive base pairs)
220
+ stems = []
221
+ current_stem = []
222
+
223
+ for i, (start, end) in enumerate(pairs):
224
+ if (i == 0 or start != pairs[i - 1][0] + 1 or end != pairs[i - 1][1] - 1) and current_stem:
225
+ stems.append(current_stem)
226
+ current_stem = []
227
+ current_stem.append((start, end))
228
+
229
+ if current_stem:
230
+ stems.append(current_stem)
231
+
232
+ # Calculate stem lengths
233
+ stem_lengths = [len(stem) for stem in stems]
234
+
235
+ # Calculate loop sizes
236
+ loops = []
237
+ for i in range(len(stems)):
238
+ stem = stems[i]
239
+ last_pair = stem[-1]
240
+ next_stem_start = stems[i + 1][0][0] if i < len(stems) - 1 else len(dot_bracket_structure)
241
+ loop_size = next_stem_start - last_pair[1] - 1
242
+ if loop_size > 0:
243
+ loops.append(loop_size)
244
+
245
+ # Calculate base pair statistics
246
+ total_paired_bases = len(pairs) * 2
247
+ total_unpaired_bases = len(dot_bracket_structure) - total_paired_bases
248
+
249
+ # Calculate simplified free energy if sequence is provided
250
+ stem_energies = []
251
+ if sequence and len(stems) > 0:
252
+ # Simplified energy parameters for nearest-neighbor model
253
+ # Values are approximate and simplified for illustration
254
+ energy_params = {
255
+ "AU": -0.9,
256
+ "UA": -0.9,
257
+ "GC": -2.1,
258
+ "CG": -2.1,
259
+ "GU": -0.5,
260
+ "UG": -0.5,
261
+ }
262
+
263
+ for stem in stems:
264
+ stem_energy = 0
265
+ for start, end in stem:
266
+ if start < len(sequence) and end < len(sequence):
267
+ pair = sequence[start] + sequence[end]
268
+ stem_energy += energy_params.get(pair, 0)
269
+ stem_energies.append(stem_energy)
270
+
271
+ # Prepare results
272
+ log += "\n## Structural Features\n\n"
273
+ log += f"Total base pairs: {len(pairs)}\n"
274
+ log += f"Number of stems: {len(stems)}\n"
275
+ log += f"Longest stem length: {max(stem_lengths) if stem_lengths else 0}\n"
276
+ log += f"Average stem length: {sum(stem_lengths) / len(stem_lengths) if stem_lengths else 0:.2f}\n"
277
+ log += f"Paired bases: {total_paired_bases} ({total_paired_bases / len(dot_bracket_structure) * 100:.1f}%)\n"
278
+ log += f"Unpaired bases: {total_unpaired_bases} ({total_unpaired_bases / len(dot_bracket_structure) * 100:.1f}%)\n"
279
+
280
+ if loops:
281
+ log += f"Number of loops: {len(loops)}\n"
282
+ log += f"Average loop size: {sum(loops) / len(loops):.2f}\n"
283
+ log += f"Largest loop size: {max(loops)}\n"
284
+
285
+ if sequence and stem_energies:
286
+ log += "\n## Energy Calculations\n\n"
287
+ log += f"Total estimated free energy: {sum(stem_energies):.2f} kcal/mol\n"
288
+
289
+ if len(stems) >= 2:
290
+ log += f"Upstream stem free energy: {stem_energies[0]:.2f} kcal/mol\n"
291
+ log += f"Downstream stem free energy: {stem_energies[-1]:.2f} kcal/mol\n"
292
+
293
+ # If the first stem is the "zipper" stem
294
+ if stem_lengths and stem_lengths[0] >= 3:
295
+ log += f"Zipper stem free energy: {stem_energies[0]:.2f} kcal/mol\n"
296
+
297
+ log += "\n## Stem Details\n\n"
298
+ for i, stem in enumerate(stems):
299
+ log += f"Stem {i + 1}: {len(stem)} base pairs\n"
300
+ log += f" Positions: {stem[0][0]}-{stem[0][1]} to {stem[-1][0]}-{stem[-1][1]}\n"
301
+ if sequence and i < len(stem_energies):
302
+ log += f" Estimated stability: {stem_energies[i]:.2f} kcal/mol\n"
303
+
304
+ return log
305
+
306
+
307
+ def analyze_protease_kinetics(
308
+ time_points,
309
+ fluorescence_data,
310
+ substrate_concentrations,
311
+ enzyme_concentration,
312
+ output_prefix="protease_kinetics",
313
+ output_dir="./",
314
+ ):
315
+ """Analyze protease kinetics data from fluorogenic peptide cleavage assays.
316
+
317
+ This function processes time-course fluorescence data from protease-mediated peptide
318
+ cleavage assays, fits the data to Michaelis-Menten kinetics, and determines key
319
+ kinetic parameters (kcat, KM, and catalytic efficiency).
320
+
321
+ Parameters
322
+ ----------
323
+ time_points : numpy.ndarray
324
+ Array of time points (in seconds) at which measurements were taken
325
+
326
+ fluorescence_data : numpy.ndarray
327
+ 2D array of fluorescence measurements where each row corresponds to a different
328
+ substrate concentration and each column corresponds to a time point
329
+
330
+ substrate_concentrations : numpy.ndarray
331
+ Array of substrate concentrations (in μM) corresponding to each row in fluorescence_data
332
+
333
+ enzyme_concentration : float
334
+ Concentration of the protease enzyme (in μM)
335
+
336
+ output_prefix : str, optional
337
+ Prefix for output files (default: "protease_kinetics")
338
+
339
+ output_dir : str, optional
340
+ Directory to save output files (default: "./")
341
+
342
+ Returns
343
+ -------
344
+ str
345
+ A research log summarizing the analysis steps and results
346
+
347
+ """
348
+ import os
349
+
350
+ import matplotlib.pyplot as plt
351
+ import numpy as np
352
+ from scipy.optimize import curve_fit
353
+
354
+ # Ensure output directory exists
355
+ if not os.path.exists(output_dir):
356
+ os.makedirs(output_dir)
357
+
358
+ # Create full output paths
359
+ plot_filename = os.path.join(output_dir, f"{output_prefix}_mm_plot.png")
360
+ results_filename = os.path.join(output_dir, f"{output_prefix}_results.txt")
361
+
362
+ # Step 1: Calculate initial velocities for each substrate concentration
363
+ initial_velocities = np.zeros(len(substrate_concentrations))
364
+
365
+ for i, fluorescence_curve in enumerate(fluorescence_data):
366
+ # Linear fit to the initial portion of the curve (first 20% of time points or at least 5 points)
367
+ num_points = max(5, int(len(time_points) * 0.2))
368
+ slope, _ = np.polyfit(time_points[:num_points], fluorescence_curve[:num_points], 1)
369
+ initial_velocities[i] = slope
370
+
371
+ # Step 2: Define Michaelis-Menten equation for curve fitting
372
+ def michaelis_menten(s, vmax, km):
373
+ return vmax * s / (km + s)
374
+
375
+ # Step 3: Fit the data to the Michaelis-Menten equation
376
+ try:
377
+ params, covariance = curve_fit(
378
+ michaelis_menten,
379
+ substrate_concentrations,
380
+ initial_velocities,
381
+ p0=[max(initial_velocities), np.mean(substrate_concentrations)],
382
+ bounds=([0, 0], [np.inf, np.inf]),
383
+ )
384
+
385
+ vmax, km = params
386
+ std_dev = np.sqrt(np.diag(covariance))
387
+ vmax_std, km_std = std_dev
388
+
389
+ # Step 4: Calculate kcat and catalytic efficiency
390
+ kcat = vmax / enzyme_concentration
391
+ kcat_std = vmax_std / enzyme_concentration
392
+ catalytic_efficiency = kcat / km
393
+ catalytic_efficiency_std = catalytic_efficiency * np.sqrt((kcat_std / kcat) ** 2 + (km_std / km) ** 2)
394
+
395
+ # Step 5: Create a plot and save it
396
+ plt.figure(figsize=(10, 6))
397
+ plt.scatter(
398
+ substrate_concentrations,
399
+ initial_velocities,
400
+ color="blue",
401
+ label="Experimental data",
402
+ )
403
+
404
+ # Generate smooth curve for the fitted model
405
+ s_curve = np.linspace(0, max(substrate_concentrations) * 1.2, 100)
406
+ v_curve = michaelis_menten(s_curve, vmax, km)
407
+ plt.plot(s_curve, v_curve, "r-", label="Michaelis-Menten fit")
408
+
409
+ plt.xlabel("Substrate Concentration (μM)")
410
+ plt.ylabel("Initial Velocity (a.u./s)")
411
+ plt.title("Michaelis-Menten Kinetics")
412
+ plt.legend()
413
+ plt.grid(True, alpha=0.3)
414
+
415
+ plt.savefig(plot_filename)
416
+ plt.close()
417
+
418
+ # Step 6: Save numerical results to a file
419
+ with open(results_filename, "w") as f:
420
+ f.write("Protease Kinetics Analysis Results\n")
421
+ f.write("==================================\n\n")
422
+ f.write(f"Vmax: {vmax:.4f} ± {vmax_std:.4f} a.u./s\n")
423
+ f.write(f"KM: {km:.4f} ± {km_std:.4f} μM\n")
424
+ f.write(f"kcat: {kcat:.4f} ± {kcat_std:.4f} s^-1\n")
425
+ f.write(
426
+ f"Catalytic efficiency (kcat/KM): {catalytic_efficiency:.4f} ± {catalytic_efficiency_std:.4f} μM^-1 s^-1\n"
427
+ )
428
+
429
+ # Step 7: Create research log
430
+ research_log = f"""
431
+ Protease Kinetics Analysis Research Log
432
+ ======================================
433
+
434
+ Analysis Steps:
435
+ 1. Calculated initial velocities from time-course fluorescence data for {len(substrate_concentrations)} different substrate concentrations
436
+ 2. Fitted initial velocities to the Michaelis-Menten equation using non-linear regression
437
+ 3. Determined kinetic parameters and their uncertainties
438
+
439
+ Results:
440
+ - Vmax: {vmax:.4f} ± {vmax_std:.4f} a.u./s
441
+ - KM: {km:.4f} ± {km_std:.4f} μM
442
+ - kcat: {kcat:.4f} ± {kcat_std:.4f} s^-1
443
+ - Catalytic efficiency (kcat/KM): {catalytic_efficiency:.4f} ± {catalytic_efficiency_std:.4f} μM^-1 s^-1
444
+
445
+ Files Generated:
446
+ 1. {plot_filename} - Michaelis-Menten plot showing experimental data and fitted curve
447
+ 2. {results_filename} - Text file containing detailed results
448
+
449
+ Analysis completed successfully.
450
+ """
451
+
452
+ return research_log
453
+
454
+ except Exception as e:
455
+ return f"Error during analysis: {str(e)}"
456
+
457
+
458
+ def analyze_enzyme_kinetics_assay(
459
+ enzyme_name,
460
+ substrate_concentrations,
461
+ enzyme_concentration,
462
+ modulators=None,
463
+ time_points=None,
464
+ output_dir="./",
465
+ ):
466
+ """Performs in vitro enzyme kinetics assay and analyzes the dose-dependent effects of modulators.
467
+
468
+ Parameters
469
+ ----------
470
+ enzyme_name : str
471
+ Name of the purified enzyme being tested
472
+ substrate_concentrations : list or numpy.ndarray
473
+ List of substrate concentrations in μM for kinetic analysis
474
+ enzyme_concentration : float
475
+ Concentration of the enzyme in nM
476
+ modulators : dict, optional
477
+ Dictionary of modulators where keys are modulator names and values are lists of
478
+ concentrations in μM. Default is None (no modulators).
479
+ time_points : list or numpy.ndarray, optional
480
+ Time points in minutes for time-course measurements. Default is None, which uses
481
+ [0, 5, 10, 15, 20, 30, 45, 60].
482
+ output_dir : str, optional
483
+ Directory to save output files. Default is current directory.
484
+
485
+ Returns
486
+ -------
487
+ str
488
+ Research log summarizing the enzyme kinetics assay procedure and results
489
+
490
+ """
491
+ import csv
492
+ import os
493
+
494
+ import numpy as np
495
+ from scipy.optimize import curve_fit
496
+
497
+ # Create output directory if it doesn't exist
498
+ if not os.path.exists(output_dir):
499
+ os.makedirs(output_dir)
500
+
501
+ # Set default time points if not provided
502
+ if time_points is None:
503
+ time_points = np.array([0, 5, 10, 15, 20, 30, 45, 60])
504
+ else:
505
+ # Ensure time_points is a numpy array
506
+ time_points = np.array(time_points)
507
+
508
+ # Initialize research log
509
+ log = f"## In Vitro Enzyme Kinetics Assay: {enzyme_name}\n\n"
510
+ log += f"Enzyme concentration: {enzyme_concentration} nM\n"
511
+
512
+ # Michaelis-Menten equation for curve fitting
513
+ def michaelis_menten(s, vmax, km):
514
+ return vmax * s / (km + s)
515
+
516
+ # 1. Time-course kinetic assay
517
+ log += "\n### Time-Course Kinetic Assay\n\n"
518
+ log += "Measuring enzyme activity over time to establish linear range.\n"
519
+
520
+ # Simulate time-course data (realistic enzyme kinetics with some noise)
521
+ # Using a simple exponential approach to equilibrium model
522
+ max_activity = 100 # arbitrary units
523
+ rate_constant = 0.05 # min^-1
524
+
525
+ # Simulate enzyme activity over time with some noise
526
+ np.random.seed(42) # For reproducibility
527
+ time_course_activity = max_activity * (1 - np.exp(-rate_constant * time_points))
528
+ time_course_activity += np.random.normal(0, 3, len(time_points)) # Add noise
529
+
530
+ # Save time-course data
531
+ time_course_file = os.path.join(output_dir, f"{enzyme_name}_time_course.csv")
532
+ with open(time_course_file, "w", newline="") as f:
533
+ writer = csv.writer(f)
534
+ writer.writerow(["Time (min)", "Activity (units)"])
535
+ for t, a in zip(time_points, time_course_activity, strict=False):
536
+ writer.writerow([t, a])
537
+
538
+ # Determine linear range (first ~30% of the curve)
539
+ linear_cutoff_index = np.where(time_course_activity >= 0.3 * max_activity)[0][0]
540
+ linear_time = time_points[: linear_cutoff_index + 1]
541
+
542
+ log += f"Time-course data saved to: {time_course_file}\n"
543
+ log += f"Linear range determined to be 0-{linear_time[-1]} minutes.\n"
544
+
545
+ # 2. Substrate kinetics (Michaelis-Menten analysis)
546
+ log += "\n### Substrate Kinetics Analysis\n\n"
547
+
548
+ # Simulate enzyme activity at different substrate concentrations
549
+ # based on Michaelis-Menten kinetics
550
+ true_vmax = 120 # arbitrary units
551
+ true_km = 25 # μM
552
+
553
+ activity_values = michaelis_menten(np.array(substrate_concentrations), true_vmax, true_km)
554
+ activity_values += np.random.normal(0, 5, len(substrate_concentrations)) # Add noise
555
+
556
+ # Fit data to Michaelis-Menten equation
557
+ try:
558
+ params, _ = curve_fit(
559
+ michaelis_menten,
560
+ substrate_concentrations,
561
+ activity_values,
562
+ p0=[100, 20],
563
+ bounds=([0, 0], [500, 200]),
564
+ )
565
+ vmax, km = params
566
+
567
+ # Save substrate kinetics data
568
+ kinetics_file = os.path.join(output_dir, f"{enzyme_name}_substrate_kinetics.csv")
569
+ with open(kinetics_file, "w", newline="") as f:
570
+ writer = csv.writer(f)
571
+ writer.writerow(["Substrate (μM)", "Activity (units)"])
572
+ for s, a in zip(substrate_concentrations, activity_values, strict=False):
573
+ writer.writerow([s, a])
574
+
575
+ log += "Michaelis-Menten parameters:\n"
576
+ log += f"- Vmax: {vmax:.2f} units\n"
577
+ log += f"- Km: {km:.2f} μM\n"
578
+ log += f"Substrate kinetics data saved to: {kinetics_file}\n"
579
+ except Exception:
580
+ log += "Error: Could not fit data to Michaelis-Menten model.\n"
581
+
582
+ # 3. Modulator effects (if provided)
583
+ if modulators:
584
+ log += "\n### Modulator Effects Analysis\n\n"
585
+
586
+ for modulator_name, concentrations in modulators.items():
587
+ log += f"#### Testing modulator: {modulator_name}\n\n"
588
+
589
+ # Choose a fixed substrate concentration near Km for inhibition studies
590
+ substrate_conc = true_km
591
+
592
+ # Simulate modulator effects (e.g., competitive inhibition)
593
+ # For simplicity, using a sigmoidal dose-response curve
594
+ ic50 = np.random.uniform(1, 50) # Random IC50 between 1-50 μM
595
+ hill_coef = 1.0 # Hill coefficient
596
+
597
+ # Calculate normalized activity (% of control)
598
+ michaelis_menten(substrate_conc, true_vmax, true_km)
599
+
600
+ # Calculate activities at different modulator concentrations
601
+ modulator_activities = []
602
+ for conc in concentrations:
603
+ # Sigmoidal dose-response curve
604
+ if conc == 0:
605
+ activity = 100 # 100% activity at zero concentration
606
+ else:
607
+ activity = 100 / (1 + (conc / ic50) ** hill_coef)
608
+
609
+ # Add some noise
610
+ activity += np.random.normal(0, 3)
611
+ modulator_activities.append(activity)
612
+
613
+ # Save modulator data
614
+ modulator_file = os.path.join(output_dir, f"{enzyme_name}_{modulator_name}_dose_response.csv")
615
+ with open(modulator_file, "w", newline="") as f:
616
+ writer = csv.writer(f)
617
+ writer.writerow([f"{modulator_name} (μM)", "Activity (% of control)"])
618
+ for conc, act in zip(concentrations, modulator_activities, strict=False):
619
+ writer.writerow([conc, act])
620
+
621
+ # Calculate IC50 using curve fitting if there are enough data points
622
+ if len(concentrations) >= 4:
623
+ try:
624
+ # Define dose-response curve function
625
+ def dose_response(x, ic50, hill):
626
+ return 100 / (1 + (x / ic50) ** hill)
627
+
628
+ # Filter out zero concentration
629
+ nonzero_conc = np.array([c for c in concentrations if c > 0])
630
+ nonzero_act = np.array(
631
+ [a for c, a in zip(concentrations, modulator_activities, strict=False) if c > 0]
632
+ )
633
+
634
+ if len(nonzero_conc) >= 3: # Need at least 3 points for fitting
635
+ params, _ = curve_fit(
636
+ dose_response,
637
+ nonzero_conc,
638
+ nonzero_act,
639
+ p0=[10, 1],
640
+ bounds=([0.1, 0.1], [1000, 10]),
641
+ )
642
+ calc_ic50, calc_hill = params
643
+
644
+ log += f"Dose-response analysis for {modulator_name}:\n"
645
+ log += f"- IC50: {calc_ic50:.2f} μM\n"
646
+ log += f"- Hill coefficient: {calc_hill:.2f}\n"
647
+ else:
648
+ log += f"Insufficient non-zero data points to calculate IC50 for {modulator_name}.\n"
649
+ except Exception:
650
+ log += f"Error: Could not fit dose-response curve for {modulator_name}.\n"
651
+ else:
652
+ log += f"Insufficient data points to calculate IC50 for {modulator_name}.\n"
653
+
654
+ log += f"Dose-response data for {modulator_name} saved to: {modulator_file}\n\n"
655
+
656
+ # 4. Summary
657
+ log += "\n### Summary\n\n"
658
+ log += f"Completed in vitro enzyme kinetics assay for {enzyme_name}.\n"
659
+ log += f"- Determined linear range for time-course measurements: 0-{linear_time[-1]} minutes\n"
660
+ log += f"- Characterized Michaelis-Menten kinetics (Vmax: {vmax:.2f} units, Km: {km:.2f} μM)\n"
661
+
662
+ if modulators:
663
+ log += "- Analyzed the effects of modulators:\n"
664
+ for modulator_name in modulators:
665
+ log += f" * {modulator_name}: Data saved to {enzyme_name}_{modulator_name}_dose_response.csv\n"
666
+
667
+ return log
668
+
669
+
670
+ def analyze_itc_binding_thermodynamics(
671
+ itc_data_path=None,
672
+ itc_data=None,
673
+ temperature=298.15,
674
+ protein_concentration=None,
675
+ ligand_concentration=None,
676
+ ):
677
+ """Analyzes isothermal titration calorimetry (ITC) data to determine binding affinity and thermodynamic parameters.
678
+
679
+ Parameters
680
+ ----------
681
+ itc_data_path : str, optional
682
+ Path to CSV or TSV file containing ITC thermogram data with columns for injection number,
683
+ injection volume, and heat released/absorbed. Expected columns: 'injection', 'volume', 'heat'
684
+ itc_data : numpy.ndarray, optional
685
+ Raw ITC thermogram data as a numpy array.
686
+ Expected shape: (n_injections, 3) with columns for injection number, injection volume, and heat
687
+ This parameter is provided for backward compatibility and will be deprecated.
688
+ temperature : float, optional
689
+ Temperature in Kelvin at which the experiment was conducted. Default is 298.15 K (25°C).
690
+ protein_concentration : float, optional
691
+ Initial concentration of protein in the cell in molar (M). Required for accurate fitting.
692
+ ligand_concentration : float, optional
693
+ Concentration of ligand in the syringe in molar (M). Required for accurate fitting.
694
+
695
+ Returns
696
+ -------
697
+ str
698
+ A research log summarizing the analysis steps and results, including binding affinity (Kd),
699
+ binding enthalpy (ΔH), binding entropy (ΔS), and Gibbs free energy (ΔG).
700
+
701
+ """
702
+ import datetime
703
+
704
+ import numpy as np
705
+ import pandas as pd
706
+ from scipy.optimize import curve_fit
707
+
708
+ log = []
709
+ log.append(f"# ITC Binding Affinity Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}")
710
+ log.append("\n## Data Preprocessing")
711
+
712
+ # Check if we have data to process
713
+ if itc_data_path is None and itc_data is None:
714
+ log.append("Error: No data provided. Please provide either itc_data_path or itc_data.")
715
+ return "\n".join(log)
716
+
717
+ # Load data from file if path is provided
718
+ if itc_data_path is not None:
719
+ try:
720
+ if itc_data_path.endswith(".csv"):
721
+ loaded_data = pd.read_csv(itc_data_path)
722
+ elif itc_data_path.endswith((".tsv", ".txt")):
723
+ loaded_data = pd.read_csv(itc_data_path, sep="\t")
724
+ else:
725
+ log.append("Error: Unsupported file format. Please provide a CSV or TSV file.")
726
+ return "\n".join(log)
727
+
728
+ log.append(f"- Loaded data from file: {itc_data_path}")
729
+ log.append(f"- Input data: DataFrame with {len(loaded_data)} injections")
730
+
731
+ if all(col in loaded_data.columns for col in ["injection", "volume", "heat"]):
732
+ data = loaded_data[["injection", "volume", "heat"]].values
733
+ else:
734
+ log.append("- Error: DataFrame must contain 'injection', 'volume', and 'heat' columns")
735
+ return "\n".join(log)
736
+ except Exception as e:
737
+ log.append(f"- Error loading data from file: {str(e)}")
738
+ return "\n".join(log)
739
+ # Use provided numpy array data
740
+ elif itc_data is not None:
741
+ if isinstance(itc_data, pd.DataFrame):
742
+ log.append("Warning: Passing DataFrame directly is deprecated. Please use itc_data_path instead.")
743
+ log.append(f"- Input data: DataFrame with {len(itc_data)} injections")
744
+ if all(col in itc_data.columns for col in ["injection", "volume", "heat"]):
745
+ data = itc_data[["injection", "volume", "heat"]].values
746
+ else:
747
+ log.append("- Error: DataFrame must contain 'injection', 'volume', and 'heat' columns")
748
+ return "\n".join(log)
749
+ else:
750
+ log.append(f"- Input data: Array with {len(itc_data)} injections")
751
+ data = np.array(itc_data)
752
+
753
+ # Check if concentration data is provided
754
+ if protein_concentration is None or ligand_concentration is None:
755
+ log.append("- Warning: Protein or ligand concentration not provided. Using normalized data for fitting.")
756
+ protein_concentration = 1.0
757
+ ligand_concentration = 10.0
758
+
759
+ # Extract data
760
+ injections = data[:, 0]
761
+ volumes = data[:, 1]
762
+ heats = data[:, 2]
763
+
764
+ log.append(f"- Processed {len(injections)} injections")
765
+
766
+ # Calculate molar ratio for each injection
767
+ cell_volume = 1.4 # Default cell volume in mL (typical for ITC)
768
+
769
+ # Calculate cumulative ligand added
770
+ cumulative_volume = np.cumsum(volumes)
771
+ dilution_factor = 1 - (cumulative_volume / cell_volume)
772
+ protein_conc_corrected = protein_concentration * dilution_factor
773
+
774
+ # Calculate molar ratio [ligand]/[protein] for each injection
775
+ molar_ratios = np.zeros_like(injections, dtype=float)
776
+ for i in range(len(injections)):
777
+ ligand_added = volumes[i] * ligand_concentration / 1000 # Convert to mmol
778
+ if i == 0:
779
+ molar_ratios[i] = ligand_added / (protein_concentration * cell_volume / 1000) # Convert to mmol
780
+ else:
781
+ # Account for dilution and previous injections
782
+ molar_ratios[i] = molar_ratios[i - 1] + ligand_added / (
783
+ protein_conc_corrected[i] * (cell_volume - cumulative_volume[i]) / 1000
784
+ )
785
+
786
+ log.append("\n## Model Fitting")
787
+ log.append("- Applying one-site binding model to the ITC data")
788
+
789
+ # Define one-site binding model function
790
+ def one_site_model(x, Kd, dH, n):
791
+ """One-site binding model for ITC data.
792
+
793
+ Parameters
794
+ ----------
795
+ x: Molar ratio [ligand]/[protein]
796
+ Kd: Dissociation constant (M)
797
+ dH: Enthalpy change (cal/mol)
798
+ n: Stoichiometry
799
+
800
+ Returns: Heat per injection
801
+
802
+ """
803
+ # Convert x to fraction bound using the binding equation
804
+ Ka = 1 / Kd # Association constant
805
+ protein = protein_conc_corrected
806
+
807
+ # Calculate heat for each injection
808
+ q = np.zeros_like(x)
809
+ for i in range(len(x)):
810
+ if i == 0:
811
+ # First injection
812
+ bound = (n * protein[i] * Ka * (x[i] * protein[i])) / (1 + Ka * (x[i] * protein[i]))
813
+ q[i] = bound * dH * cell_volume
814
+ else:
815
+ # Subsequent injections (differential heat)
816
+ bound_prev = (n * protein[i - 1] * Ka * (x[i - 1] * protein[i - 1])) / (
817
+ 1 + Ka * (x[i - 1] * protein[i - 1])
818
+ )
819
+ bound_curr = (n * protein[i] * Ka * (x[i] * protein[i])) / (1 + Ka * (x[i] * protein[i]))
820
+ q[i] = bound_curr * dH * (cell_volume - cumulative_volume[i]) - bound_prev * dH * (
821
+ cell_volume - cumulative_volume[i - 1]
822
+ )
823
+
824
+ return q
825
+
826
+ # Initial parameter guesses
827
+ p0 = [1e-6, -5000, 1.0] # Kd (M), dH (cal/mol), n (stoichiometry)
828
+
829
+ try:
830
+ # Fit the model to the data
831
+ popt, pcov = curve_fit(one_site_model, molar_ratios, heats, p0=p0, maxfev=10000)
832
+ Kd, dH, n = popt
833
+
834
+ # Calculate standard errors
835
+ perr = np.sqrt(np.diag(pcov))
836
+ Kd_err, dH_err, n_err = perr
837
+
838
+ # Calculate other thermodynamic parameters
839
+ R = 1.9872 # Gas constant in cal/(mol·K)
840
+ dG = R * temperature * np.log(Kd) # Gibbs free energy
841
+ dS = (dH - dG) / temperature # Entropy
842
+
843
+ log.append("- Model fitting successful")
844
+ log.append("\n## Results")
845
+ log.append(f"- Binding Stoichiometry (n): {n:.2f} ± {n_err:.2f}")
846
+ log.append(f"- Dissociation Constant (Kd): {Kd * 1e6:.2f} ± {Kd_err * 1e6:.2f} μM")
847
+ log.append(f"- Association Constant (Ka): {1 / Kd / 1e6:.2f} × 10^6 M^-1")
848
+ log.append(f"- Binding Enthalpy (ΔH): {dH:.2f} ± {dH_err:.2f} cal/mol")
849
+ log.append(f"- Binding Entropy (ΔS): {dS:.2f} cal/(mol·K)")
850
+ log.append(f"- Gibbs Free Energy (ΔG): {dG:.2f} cal/mol")
851
+
852
+ # Calculate goodness of fit
853
+ residuals = heats - one_site_model(molar_ratios, *popt)
854
+ ss_res = np.sum(residuals**2)
855
+ ss_tot = np.sum((heats - np.mean(heats)) ** 2)
856
+ r_squared = 1 - (ss_res / ss_tot)
857
+ log.append(f"- R-squared: {r_squared:.4f}")
858
+
859
+ except Exception as e:
860
+ log.append(f"- Error during model fitting: {str(e)}")
861
+ log.append("- Consider trying different initial parameter guesses or a different binding model")
862
+
863
+ log.append("\n## Conclusion")
864
+ log.append("- Analysis complete. The thermodynamic parameters have been estimated using a one-site binding model.")
865
+ log.append(
866
+ "- For more complex binding scenarios, consider using multi-site binding models or specialized ITC analysis software."
867
+ )
868
+
869
+ return "\n".join(log)
870
+
871
+
872
+ def analyze_protein_conservation(protein_sequences, output_dir="./"):
873
+ """Perform multiple sequence alignment and phylogenetic analysis to identify conserved protein regions.
874
+
875
+ Parameters
876
+ ----------
877
+ protein_sequences : list of str
878
+ List of protein sequences in FASTA format from multiple organisms.
879
+ output_dir : str, optional
880
+ Directory to save output files (default: "./output")
881
+
882
+ Returns
883
+ -------
884
+ str
885
+ Research log summarizing the analysis steps and results, including filenames of saved outputs.
886
+
887
+ """
888
+ import os
889
+
890
+ from Bio import AlignIO, Phylo, SeqIO
891
+ from Bio.Align.Applications import MuscleCommandline
892
+ from Bio.Phylo.TreeConstruction import DistanceCalculator, DistanceTreeConstructor
893
+
894
+ # Create a research log
895
+ log = []
896
+ log.append("# Protein Sequence Alignment and Conservation Analysis")
897
+
898
+ # Ensure output directory exists
899
+ if not os.path.exists(output_dir):
900
+ os.makedirs(output_dir)
901
+
902
+ # Step 1: Save input sequences to a temporary file
903
+ log.append("\n## Step 1: Preparing Input Sequences")
904
+ input_file = os.path.join(output_dir, "input.fasta")
905
+
906
+ # Check if input is already in FASTA format or needs conversion
907
+ if isinstance(protein_sequences, list):
908
+ if all(">" in seq for seq in protein_sequences):
909
+ # Already in FASTA format
910
+ with open(input_file, "w") as f:
911
+ f.write("\n".join(protein_sequences))
912
+ else:
913
+ # Convert to FASTA format
914
+ with open(input_file, "w") as f:
915
+ for i, seq in enumerate(protein_sequences):
916
+ f.write(f">Sequence_{i + 1}\n{seq}\n")
917
+ else:
918
+ # Assume it's a single string in FASTA format
919
+ with open(input_file, "w") as f:
920
+ f.write(protein_sequences)
921
+
922
+ log.append(f"Input sequences saved to {input_file}")
923
+ log.append(f"Number of sequences: {len(list(SeqIO.parse(input_file, 'fasta')))}")
924
+
925
+ # Step 2: Perform multiple sequence alignment using MUSCLE
926
+ log.append("\n## Step 2: Multiple Sequence Alignment")
927
+ aligned_file = os.path.join(output_dir, "aligned.fasta")
928
+
929
+ try:
930
+ # Run MUSCLE for multiple sequence alignment
931
+ muscle_cline = MuscleCommandline(input=input_file, out=aligned_file)
932
+ stdout, stderr = muscle_cline()
933
+ log.append("Multiple sequence alignment completed using MUSCLE")
934
+ log.append(f"Alignment saved to {aligned_file}")
935
+
936
+ # Load the alignment
937
+ alignment = AlignIO.read(aligned_file, "fasta")
938
+ log.append(f"Alignment length: {alignment.get_alignment_length()} positions")
939
+
940
+ except Exception as e:
941
+ log.append(f"MUSCLE alignment failed: {str(e)}")
942
+ log.append("Attempting to use Biopython's built-in alignment methods...")
943
+
944
+ # Fallback to a simpler approach without pairwise2
945
+ from Bio.Align import MultipleSeqAlignment
946
+ from Bio.Seq import Seq
947
+ from Bio.SeqRecord import SeqRecord
948
+
949
+ sequences = list(SeqIO.parse(input_file, "fasta"))
950
+
951
+ # Simple approach: just pad sequences to the same length
952
+ max_length = max(len(seq.seq) for seq in sequences)
953
+ alignments = []
954
+
955
+ for seq in sequences:
956
+ # Pad sequence to max length
957
+ padded_seq = str(seq.seq).ljust(max_length, "-")
958
+ new_seq = SeqRecord(Seq(padded_seq), id=seq.id, description=seq.description)
959
+ alignments.append(new_seq)
960
+
961
+ # Create and save the alignment
962
+ msa = MultipleSeqAlignment(alignments)
963
+ AlignIO.write(msa, aligned_file, "fasta")
964
+ alignment = msa
965
+ log.append("Simple padding alignment completed as fallback method")
966
+ log.append(f"Alignment saved to {aligned_file}")
967
+
968
+ # Step 3: Generate a phylogenetic tree
969
+ log.append("\n## Step 3: Phylogenetic Analysis")
970
+ tree_file = os.path.join(output_dir, "tree.newick")
971
+
972
+ # Calculate distance matrix
973
+ calculator = DistanceCalculator("identity")
974
+ dm = calculator.get_distance(alignment)
975
+
976
+ # Construct the phylogenetic tree using neighbor-joining method
977
+ constructor = DistanceTreeConstructor()
978
+ tree = constructor.nj(dm)
979
+
980
+ # Save the tree
981
+ Phylo.write(tree, tree_file, "newick")
982
+ log.append("Phylogenetic tree constructed using neighbor-joining method")
983
+ log.append(f"Tree saved to {tree_file}")
984
+
985
+ # Step 4: Analyze conserved regions
986
+ log.append("\n## Step 4: Conservation Analysis")
987
+ conservation_file = os.path.join(output_dir, "conservation.txt")
988
+
989
+ # Simple conservation analysis
990
+ alignment_length = alignment.get_alignment_length()
991
+ conserved_positions = []
992
+
993
+ with open(conservation_file, "w") as f:
994
+ f.write("Position\tConservation_Score\tConsensus\n")
995
+
996
+ for i in range(alignment_length):
997
+ # Get all amino acids at this position
998
+ column = alignment[:, i]
999
+ set(column)
1000
+
1001
+ # Calculate a simple conservation score (percentage of most common AA)
1002
+ most_common_aa = max(column, key=column.count)
1003
+ conservation_score = column.count(most_common_aa) / len(column)
1004
+
1005
+ f.write(f"{i + 1}\t{conservation_score:.2f}\t{most_common_aa}\n")
1006
+
1007
+ # Consider positions with >80% conservation as conserved
1008
+ if conservation_score > 0.8:
1009
+ conserved_positions.append(i + 1)
1010
+
1011
+ log.append(f"Conservation analysis completed and saved to {conservation_file}")
1012
+ log.append(f"Identified {len(conserved_positions)} highly conserved positions (>80% conservation)")
1013
+
1014
+ if conserved_positions:
1015
+ log.append(f"Conserved positions: {', '.join(map(str, conserved_positions[:10]))}")
1016
+ if len(conserved_positions) > 10:
1017
+ log.append(f"... and {len(conserved_positions) - 10} more")
1018
+
1019
+ # Final summary
1020
+ log.append("\n## Summary")
1021
+ log.append("The analysis successfully completed with the following outputs:")
1022
+ log.append(f"1. Multiple sequence alignment: {aligned_file}")
1023
+ log.append(f"2. Phylogenetic tree: {tree_file}")
1024
+ log.append(f"3. Conservation analysis: {conservation_file}")
1025
+
1026
+ return "\n".join(log)
Biomanus_upload/biomni/tool/bioengineering.py ADDED
@@ -0,0 +1,1387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def analyze_cell_migration_metrics(
2
+ image_sequence_path,
3
+ pixel_size_um=1.0,
4
+ time_interval_min=1.0,
5
+ min_track_length=10,
6
+ output_dir="./",
7
+ ):
8
+ """Analyze cell migration metrics from time-lapse microscopy images.
9
+
10
+ Parameters
11
+ ----------
12
+ image_sequence_path : str
13
+ Path to the directory containing time-lapse images or path to a multi-frame TIFF file
14
+ pixel_size_um : float
15
+ Conversion factor from pixels to micrometers (default: 1.0)
16
+ time_interval_min : float
17
+ Time interval between consecutive frames in minutes (default: 1.0)
18
+ min_track_length : int
19
+ Minimum number of frames a cell must be tracked to be included in analysis (default: 10)
20
+ output_dir : str
21
+ Directory to save output files (default: "./")
22
+
23
+ Returns
24
+ -------
25
+ str
26
+ Research log summarizing the cell migration analysis process and results
27
+
28
+ """
29
+ import os
30
+
31
+ import matplotlib.pyplot as plt
32
+ import numpy as np
33
+ import pandas as pd
34
+ import trackpy as tp
35
+ from skimage import io
36
+
37
+ # Create output directory if it doesn't exist
38
+ os.makedirs(output_dir, exist_ok=True)
39
+
40
+ # Load image sequence
41
+ if os.path.isdir(image_sequence_path):
42
+ # Load from directory of images
43
+ image_files = sorted(
44
+ [f for f in os.listdir(image_sequence_path) if f.endswith((".tif", ".tiff", ".png", ".jpg", ".jpeg"))]
45
+ )
46
+ frames = [io.imread(os.path.join(image_sequence_path, f)) for f in image_files]
47
+ else:
48
+ # Load multi-frame TIFF
49
+ frames = io.imread(image_sequence_path)
50
+ if frames.ndim == 3: # Check if it's a time series
51
+ pass
52
+ else:
53
+ return "Error: Input is not a valid time-lapse image sequence"
54
+
55
+ # Step 1: Detect cells in each frame
56
+ features_list = []
57
+ for i, frame in enumerate(frames):
58
+ # Detect cells (particles) in the frame
59
+ features = tp.locate(frame, diameter=15, minmass=100)
60
+ if features is not None and not features.empty:
61
+ features["frame"] = i
62
+ features_list.append(features)
63
+
64
+ if not features_list:
65
+ return "Error: No cells detected in the image sequence"
66
+
67
+ # Combine all features
68
+ all_features = pd.concat(features_list)
69
+
70
+ # Save raw detections to output directory
71
+ raw_detections_file = os.path.join(output_dir, "raw_cell_detections.csv")
72
+ all_features.to_csv(raw_detections_file, index=False)
73
+
74
+ # Step 2: Link features into cell trajectories
75
+ trajectories = tp.link_df(all_features, search_range=10, memory=3)
76
+
77
+ # Save linked trajectories before filtering
78
+ # Reset index to ensure frame is a column, not an index level
79
+ trajectories_reset = trajectories.reset_index(drop=True)
80
+ all_trajectories_file = os.path.join(output_dir, "all_trajectories.csv")
81
+ trajectories_reset.to_csv(all_trajectories_file, index=False)
82
+
83
+ # Step 3: Filter trajectories to get only the ones that appear in enough frames
84
+ trajectories = tp.filter_stubs(trajectories, threshold=min_track_length)
85
+
86
+ if trajectories.empty:
87
+ return "No complete cell tracks found. Try adjusting parameters."
88
+
89
+ # Save filtered trajectories
90
+ filtered_trajectories_file = os.path.join(output_dir, "filtered_trajectories.csv")
91
+ # Reset index again to be safe
92
+ trajectories = trajectories.reset_index(drop=True)
93
+ trajectories.to_csv(filtered_trajectories_file, index=False)
94
+
95
+ # Step 4: Calculate migration metrics for each cell
96
+ cell_ids = trajectories["particle"].unique()
97
+ metrics = []
98
+
99
+ for cell_id in cell_ids:
100
+ cell_track = trajectories[trajectories["particle"] == cell_id].sort_values("frame")
101
+
102
+ # Convert pixel positions to micrometers
103
+ cell_track["x_um"] = cell_track["x"] * pixel_size_um
104
+ cell_track["y_um"] = cell_track["y"] * pixel_size_um
105
+
106
+ # Calculate displacements between consecutive frames
107
+ dx = np.diff(cell_track["x_um"])
108
+ dy = np.diff(cell_track["y_um"])
109
+
110
+ # Calculate step distances
111
+ step_distances = np.sqrt(dx**2 + dy**2)
112
+
113
+ # Calculate total path length
114
+ path_length = np.sum(step_distances)
115
+
116
+ # Calculate net displacement (straight-line distance from start to end)
117
+ start_x, start_y = cell_track.iloc[0][["x_um", "y_um"]]
118
+ end_x, end_y = cell_track.iloc[-1][["x_um", "y_um"]]
119
+ net_displacement = np.sqrt((end_x - start_x) ** 2 + (end_y - start_y) ** 2)
120
+
121
+ # Calculate directionality ratio (net displacement / path length)
122
+ directionality = net_displacement / path_length if path_length > 0 else 0
123
+
124
+ # Calculate speed (μm/min)
125
+ time_tracked = (cell_track["frame"].max() - cell_track["frame"].min()) * time_interval_min
126
+ speed = path_length / time_tracked if time_tracked > 0 else 0
127
+
128
+ metrics.append(
129
+ {
130
+ "cell_id": cell_id,
131
+ "frames_tracked": len(cell_track),
132
+ "speed_um_per_min": speed,
133
+ "directionality": directionality,
134
+ "displacement_um": net_displacement,
135
+ "path_length_um": path_length,
136
+ }
137
+ )
138
+
139
+ # Convert metrics to DataFrame
140
+ metrics_df = pd.DataFrame(metrics)
141
+
142
+ # Save metrics to CSV
143
+ metrics_file = os.path.join(output_dir, "cell_migration_metrics.csv")
144
+ metrics_df.to_csv(metrics_file, index=False)
145
+
146
+ # Calculate summary statistics
147
+ summary = {
148
+ "num_cells_tracked": len(metrics_df),
149
+ "avg_speed": metrics_df["speed_um_per_min"].mean(),
150
+ "std_speed": metrics_df["speed_um_per_min"].std(),
151
+ "avg_directionality": metrics_df["directionality"].mean(),
152
+ "std_directionality": metrics_df["directionality"].std(),
153
+ "avg_displacement": metrics_df["displacement_um"].mean(),
154
+ "std_displacement": metrics_df["displacement_um"].std(),
155
+ }
156
+
157
+ # Save summary statistics
158
+ summary_file = os.path.join(output_dir, "migration_summary.csv")
159
+ pd.DataFrame([summary]).to_csv(summary_file, index=False)
160
+
161
+ # Save trajectories visualization
162
+ fig, ax = plt.figure(figsize=(8, 8)), plt.gca()
163
+ tp.plot_traj(trajectories, ax=ax)
164
+ plt.title("Cell Migration Trajectories")
165
+ plt.xlabel("x position (pixels)")
166
+ plt.ylabel("y position (pixels)")
167
+
168
+ trajectories_file = os.path.join(output_dir, "cell_trajectories.png")
169
+ plt.savefig(trajectories_file)
170
+ plt.close()
171
+
172
+ # Create a rose plot to show migration directionality
173
+ fig, ax = plt.subplots(subplot_kw={"projection": "polar"}, figsize=(8, 8))
174
+
175
+ # Calculate angles for each cell (end position relative to start)
176
+ angles = []
177
+ for cell_id in cell_ids:
178
+ cell_track = trajectories[trajectories["particle"] == cell_id].sort_values("frame")
179
+ start_x, start_y = cell_track.iloc[0][["x", "y"]]
180
+ end_x, end_y = cell_track.iloc[-1][["x", "y"]]
181
+ dx, dy = end_x - start_x, end_y - start_y
182
+ angle = np.arctan2(dy, dx)
183
+ angles.append(angle)
184
+
185
+ # Plot the histogram
186
+ bins = np.linspace(-np.pi, np.pi, 16)
187
+ ax.hist(angles, bins=bins)
188
+ ax.set_title("Cell Migration Directionality")
189
+
190
+ # Save the rose plot
191
+ rose_plot_file = os.path.join(output_dir, "rose_plot.png")
192
+ plt.savefig(rose_plot_file)
193
+ plt.close()
194
+
195
+ # Create a displacement plot
196
+ plt.figure(figsize=(10, 6))
197
+ plt.bar(range(len(metrics_df)), metrics_df["displacement_um"])
198
+ plt.xlabel("Cell ID")
199
+ plt.ylabel("Displacement (μm)")
200
+ plt.title("Cell Displacements")
201
+
202
+ # Save the displacement plot
203
+ displacement_plot_file = os.path.join(output_dir, "track_displacement_plot.png")
204
+ plt.savefig(displacement_plot_file)
205
+ plt.close()
206
+
207
+ # Generate research log
208
+ log = f"""
209
+ Cell Migration Analysis Research Log:
210
+
211
+ 1. Analyzed time-lapse sequence with {len(frames)} frames
212
+ 2. Detected and tracked {len(cell_ids)} cells that persisted for at least {min_track_length} frames
213
+ 3. Calculated key migration metrics:
214
+ - Average speed: {summary["avg_speed"]:.2f} ± {summary["std_speed"]:.2f} μm/min
215
+ - Average directionality ratio: {summary["avg_directionality"]:.2f} ± {summary["std_directionality"]:.2f}
216
+ - Average displacement: {summary["avg_displacement"]:.2f} ± {summary["std_displacement"]:.2f} μm
217
+
218
+ 4. Files saved:
219
+ - Raw cell detections: {raw_detections_file}
220
+ - All cell trajectories: {all_trajectories_file}
221
+ - Filtered trajectories: {filtered_trajectories_file}
222
+ - Detailed cell metrics: {metrics_file}
223
+ - Summary statistics: {summary_file}
224
+ - Cell trajectories visualization: {trajectories_file}
225
+ - Direction rose plot: {rose_plot_file}
226
+ - Cell displacement plot: {displacement_plot_file}
227
+
228
+ Note: Analysis used pixel size of {pixel_size_um} μm and time interval of {time_interval_min} min between frames.
229
+ """
230
+
231
+ return log.strip()
232
+
233
+
234
+ def perform_crispr_cas9_genome_editing(guide_rna_sequences, target_genomic_loci, cell_tissue_type):
235
+ """Simulates CRISPR-Cas9 genome editing process including guide RNA design, delivery, and analysis.
236
+
237
+ Parameters
238
+ ----------
239
+ guide_rna_sequences : list of str
240
+ List of guide RNA sequences (20 nucleotides each) targeting the genomic region of interest
241
+
242
+ target_genomic_loci : str
243
+ Target genomic sequence to be edited (should be longer than guide RNA and contain the target sites)
244
+
245
+ cell_tissue_type : str
246
+ Type of cell or tissue being edited (affects delivery efficiency and editing outcomes)
247
+
248
+ Returns
249
+ -------
250
+ str
251
+ Research log detailing the CRISPR-Cas9 editing process, including steps taken and results
252
+
253
+ """
254
+ import os
255
+ import random
256
+ from datetime import datetime
257
+
258
+ # Initialize research log
259
+ log = "CRISPR-Cas9 Genome Editing Research Log\n"
260
+ log += f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
261
+ log += f"Cell/Tissue Type: {cell_tissue_type}\n\n"
262
+
263
+ # Step 1: Validate guide RNA sequences
264
+ log += "STEP 1: Guide RNA Validation\n"
265
+ valid_guides = []
266
+
267
+ for i, guide in enumerate(guide_rna_sequences):
268
+ if len(guide) != 20:
269
+ log += f" Guide {i + 1}: INVALID - Guide RNA must be 20 nucleotides (current length: {len(guide)})\n"
270
+ continue
271
+
272
+ if not all(n in "ATGC" for n in guide.upper()):
273
+ log += f" Guide {i + 1}: INVALID - Guide RNA contains invalid nucleotides\n"
274
+ continue
275
+
276
+ # Calculate GC content (affects guide efficiency)
277
+ gc_content = (guide.upper().count("G") + guide.upper().count("C")) / len(guide) * 100
278
+ efficiency_score = 0
279
+
280
+ if 40 <= gc_content <= 60:
281
+ efficiency_score += 1
282
+ gc_quality = "Optimal"
283
+ else:
284
+ gc_quality = "Suboptimal"
285
+
286
+ log += f" Guide {i + 1}: VALID - {guide} (GC content: {gc_content:.1f}% - {gc_quality})\n"
287
+ valid_guides.append((guide, efficiency_score))
288
+
289
+ if not valid_guides:
290
+ log += "\nNo valid guide RNAs found. Genome editing cannot proceed.\n"
291
+ return log
292
+
293
+ # Step 2: Target site identification
294
+ log += "\nSTEP 2: Target Site Identification\n"
295
+
296
+ target_seq = target_genomic_loci.upper()
297
+ target_matches = []
298
+
299
+ for i, (guide, score) in enumerate(valid_guides):
300
+ # Find guide RNA target in genomic sequence (including PAM site NGG)
301
+ guide.upper() + "NGG"
302
+
303
+ # Check if guide sequence is in target (simplified)
304
+ if guide.upper() in target_seq:
305
+ position = target_seq.find(guide.upper())
306
+ # Check if there's a PAM sequence (NGG) after the guide
307
+ if position + len(guide) + 2 <= len(target_seq):
308
+ potential_pam = target_seq[position + len(guide) : position + len(guide) + 3]
309
+ if potential_pam[1:3] == "GG":
310
+ pam_quality = "Found"
311
+ score += 2
312
+ else:
313
+ pam_quality = "Not found"
314
+ else:
315
+ pam_quality = "Out of bounds"
316
+
317
+ log += f" Guide {i + 1}: Found at position {position} (PAM: {pam_quality})\n"
318
+ target_matches.append((guide, position, score))
319
+ else:
320
+ log += f" Guide {i + 1}: No match found in target sequence\n"
321
+
322
+ if not target_matches:
323
+ log += "\nNo matching target sites found. Genome editing cannot proceed.\n"
324
+ return log
325
+
326
+ # Step 3: Simulate CRISPR-Cas9 delivery
327
+ log += "\nSTEP 3: CRISPR-Cas9 Delivery Simulation\n"
328
+
329
+ # Cell-specific delivery efficiencies (simplified model)
330
+ delivery_efficiencies = {
331
+ "hek293": 0.85,
332
+ "hela": 0.75,
333
+ "ipsc": 0.60,
334
+ "primary_neuron": 0.40,
335
+ "hematopoietic_stem_cell": 0.55,
336
+ "mouse_embryo": 0.70,
337
+ "plant_cell": 0.30,
338
+ }
339
+
340
+ # Get delivery efficiency based on cell type (default to 0.5 if unknown)
341
+ cell_type_key = cell_tissue_type.lower().replace(" ", "_")
342
+ delivery_efficiency = delivery_efficiencies.get(cell_type_key, 0.5)
343
+
344
+ log += f" Delivery method: Lipofection for {cell_tissue_type}\n"
345
+ log += f" Estimated delivery efficiency: {delivery_efficiency * 100:.1f}%\n"
346
+
347
+ # Step 4: Simulate genome editing
348
+ log += "\nSTEP 4: Genome Editing Simulation\n"
349
+
350
+ # Select best guide based on score
351
+ best_guide, best_position, best_score = sorted(target_matches, key=lambda x: x[2], reverse=True)[0]
352
+
353
+ log += f" Selected guide RNA: {best_guide} (highest efficiency score)\n"
354
+ log += f" Target position: {best_position} to {best_position + len(best_guide) - 1}\n"
355
+
356
+ # Simulate editing outcome
357
+ edit_success_rate = delivery_efficiency * (0.5 + (best_score * 0.1)) # Between 50-90% based on guide quality
358
+
359
+ # Cut site (typically 3 bases upstream of PAM)
360
+ cut_position = best_position + len(best_guide) - 3
361
+ log += f" Predicted cut site: Between positions {cut_position} and {cut_position + 1}\n"
362
+
363
+ # Simulate editing outcomes
364
+ indel_size = random.randint(1, 5) # Random indel size between 1-5 bp
365
+
366
+ # Create modified sequence (simulate a deletion for simplicity)
367
+ modified_sequence = target_seq[:cut_position] + target_seq[cut_position + indel_size :]
368
+
369
+ log += f" Simulated edit: {indel_size}bp deletion at cut site\n"
370
+ log += f" Predicted editing efficiency: {edit_success_rate * 100:.1f}%\n"
371
+
372
+ # Step 5: Analysis of editing outcomes
373
+ log += "\nSTEP 5: Editing Outcome Analysis\n"
374
+
375
+ # Calculate basic stats
376
+ log += f" Original sequence length: {len(target_seq)} bp\n"
377
+ log += f" Modified sequence length: {len(modified_sequence)} bp\n"
378
+
379
+ # Save sequences to files
380
+ os.makedirs("crispr_results", exist_ok=True)
381
+
382
+ original_file = "crispr_results/original_sequence.txt"
383
+ with open(original_file, "w") as f:
384
+ f.write(f">Original_Sequence\n{target_seq}\n")
385
+
386
+ modified_file = "crispr_results/modified_sequence.txt"
387
+ with open(modified_file, "w") as f:
388
+ f.write(f">Modified_Sequence\n{modified_sequence}\n")
389
+
390
+ log += f" Original sequence saved to: {original_file}\n"
391
+ log += f" Modified sequence saved to: {modified_file}\n"
392
+
393
+ # Summary
394
+ log += "\nSUMMARY:\n"
395
+ log += f" CRISPR-Cas9 editing successfully simulated for {cell_tissue_type}\n"
396
+ log += f" {indel_size}bp deletion introduced at position {cut_position}\n"
397
+ log += f" Expected success rate in cell population: {edit_success_rate * 100:.1f}%\n"
398
+
399
+ return log
400
+
401
+
402
+ def analyze_calcium_imaging_data(image_stack_path, output_dir="./"):
403
+ """Analyze calcium imaging data to quantify neuronal activity metrics.
404
+
405
+ This function processes fluorescence microscopy images of GCaMP-labeled neurons
406
+ to extract quantitative metrics of neuronal activity, including cell counts,
407
+ event rates, decay times, and signal-to-noise ratios.
408
+
409
+ Parameters
410
+ ----------
411
+ image_stack_path : str
412
+ Path to the time-series stack of fluorescence microscopy images (TIFF format)
413
+ output_dir : str, optional
414
+ Directory to save output files (default: "./")
415
+
416
+ Returns
417
+ -------
418
+ str
419
+ Research log summarizing the analysis steps and results
420
+
421
+ """
422
+ import os
423
+
424
+ import numpy as np
425
+ import pandas as pd
426
+ from scipy import ndimage, signal
427
+ from scipy.optimize import curve_fit
428
+ from skimage import feature, filters, io, measure, segmentation
429
+
430
+ # Create output directory if it doesn't exist
431
+ os.makedirs(output_dir, exist_ok=True)
432
+
433
+ # Step 1: Load the image stack
434
+ log = "CALCIUM IMAGING ANALYSIS LOG\n"
435
+ log += "===========================\n\n"
436
+ log += f"Loading image stack from: {image_stack_path}\n"
437
+
438
+ try:
439
+ image_stack = io.imread(image_stack_path)
440
+ num_frames, height, width = image_stack.shape
441
+ log += f"Successfully loaded {num_frames} frames of size {height}x{width}\n\n"
442
+ except Exception as e:
443
+ return f"Error loading image stack: {str(e)}"
444
+
445
+ # Step 2: Calculate mean image for segmentation
446
+ log += "Step 1: Preprocessing and neuron segmentation\n"
447
+ mean_image = np.mean(image_stack, axis=0)
448
+
449
+ # Apply Gaussian filter to reduce noise
450
+ smooth_mean = filters.gaussian(mean_image, sigma=2)
451
+
452
+ # Step 3: Segment neurons using watershed
453
+ # Find local maxima (potential cell centers)
454
+ distance = ndimage.distance_transform_edt(smooth_mean)
455
+ # Create a mask for local maxima instead of using indices=False parameter
456
+ coordinates = feature.peak_local_max(distance, min_distance=10)
457
+
458
+ # Handle case where no local maxima are found
459
+ if len(coordinates) == 0:
460
+ log += "No local maxima detected. Using simple thresholding instead.\n"
461
+ # Create a simple binary mask using thresholding as fallback
462
+ binary_mask = smooth_mean > filters.threshold_otsu(smooth_mean)
463
+ markers = measure.label(binary_mask)
464
+ else:
465
+ local_max = np.zeros_like(distance, dtype=bool)
466
+ for coord in coordinates:
467
+ local_max[coord[0], coord[1]] = True
468
+ markers = measure.label(local_max)
469
+
470
+ # Watershed segmentation
471
+ segmented = segmentation.watershed(-smooth_mean, markers, mask=smooth_mean > filters.threshold_otsu(smooth_mean))
472
+
473
+ # Get region properties
474
+ regions = measure.regionprops(segmented)
475
+ cell_count = len(regions)
476
+ log += f"Detected {cell_count} neurons in the field of view\n\n"
477
+
478
+ # Step 4: Extract time-series data for each neuron
479
+ log += "Step 2: Extracting fluorescence time-series for each neuron\n"
480
+ time_series_data = []
481
+
482
+ for _i, region in enumerate(regions):
483
+ mask = segmented == region.label
484
+ cell_time_series = []
485
+
486
+ for frame in range(num_frames):
487
+ intensity = np.mean(image_stack[frame][mask])
488
+ cell_time_series.append(intensity)
489
+
490
+ time_series_data.append(cell_time_series)
491
+
492
+ time_series_array = np.array(time_series_data)
493
+
494
+ # Step 5: Detect calcium events and calculate metrics
495
+ log += "Step 3: Calculating neuronal activity metrics\n"
496
+
497
+ # Function to fit exponential decay
498
+ def exp_decay(x, a, tau, c):
499
+ return a * np.exp(-x / tau) + c
500
+
501
+ event_rates = []
502
+ decay_times = []
503
+ snr_values = []
504
+
505
+ for i, ts in enumerate(time_series_data):
506
+ # Normalize time series
507
+ baseline = np.percentile(ts, 20)
508
+ ts_norm = [(x - baseline) / baseline for x in ts]
509
+
510
+ # Simple event detection (threshold crossing)
511
+ threshold = np.std(ts_norm) * 2
512
+ events = []
513
+ in_event = False
514
+
515
+ for j, val in enumerate(ts_norm):
516
+ if not in_event and val > threshold:
517
+ events.append(j)
518
+ in_event = True
519
+ elif in_event and val < threshold:
520
+ in_event = False
521
+
522
+ # Calculate event rate (events per minute, assuming 10 Hz acquisition)
523
+ acquisition_rate = 10 # Hz (assumption)
524
+ recording_time_minutes = num_frames / acquisition_rate / 60
525
+ event_rate = len(events) / recording_time_minutes
526
+ event_rates.append(event_rate)
527
+
528
+ # Calculate decay times for detected events
529
+ cell_decay_times = []
530
+ for event_start in events:
531
+ if event_start + 30 < len(ts_norm): # Ensure enough frames after event
532
+ event_window = ts_norm[event_start : event_start + 30]
533
+ peak_idx = np.argmax(event_window)
534
+ decay_segment = event_window[peak_idx:]
535
+
536
+ try:
537
+ # Fit exponential decay
538
+ x_data = np.arange(len(decay_segment))
539
+ popt, _ = curve_fit(
540
+ exp_decay,
541
+ x_data,
542
+ decay_segment,
543
+ p0=[decay_segment[0], 5, decay_segment[-1]],
544
+ bounds=([0, 0, 0], [np.inf, np.inf, np.inf]),
545
+ )
546
+ tau = popt[1] # Decay time constant
547
+ cell_decay_times.append(tau / acquisition_rate) # Convert to seconds
548
+ except Exception:
549
+ # Skip if curve fitting fails
550
+ pass
551
+
552
+ if cell_decay_times:
553
+ decay_times.append(np.mean(cell_decay_times))
554
+ else:
555
+ decay_times.append(np.nan)
556
+
557
+ # Calculate signal-to-noise ratio
558
+ signal = np.mean([ts_norm[e] for e in events]) if events else 0 # noqa: F811
559
+ noise = np.std([ts_norm[i] for i in range(len(ts_norm)) if all(abs(i - e) > 5 for e in events)])
560
+ snr = signal / noise if noise > 0 else 0
561
+ snr_values.append(snr)
562
+
563
+ # Step 6: Compile and save results
564
+ cell_metrics = pd.DataFrame(
565
+ {
566
+ "Cell_ID": range(1, cell_count + 1),
567
+ "Event_Rate_per_min": event_rates,
568
+ "Decay_Time_sec": decay_times,
569
+ "SNR": snr_values,
570
+ }
571
+ )
572
+
573
+ metrics_file = os.path.join(output_dir, "neuron_activity_metrics.csv")
574
+ cell_metrics.to_csv(metrics_file, index=False)
575
+
576
+ # Save time series data
577
+ time_series_file = os.path.join(output_dir, "neuron_time_series.csv")
578
+ time_series_df = pd.DataFrame(time_series_array.T)
579
+ time_series_df.columns = [f"Cell_{i + 1}" for i in range(cell_count)]
580
+ time_series_df.to_csv(time_series_file, index=False)
581
+
582
+ # Step 7: Summarize results
583
+ log += f"Cell count: {cell_count}\n"
584
+ log += f"Average event rate: {np.nanmean(event_rates):.2f} events/min\n"
585
+ log += f"Average decay time: {np.nanmean(decay_times):.2f} seconds\n"
586
+ log += f"Average SNR: {np.nanmean(snr_values):.2f}\n\n"
587
+
588
+ log += "Step 4: Results saved to files\n"
589
+ log += f"Neuron activity metrics saved to: {metrics_file}\n"
590
+ log += f"Time series data saved to: {time_series_file}\n"
591
+
592
+ return log
593
+
594
+
595
+ def analyze_in_vitro_drug_release_kinetics(
596
+ time_points,
597
+ concentration_data,
598
+ drug_name="Drug",
599
+ total_drug_loaded=None,
600
+ output_dir="./",
601
+ ):
602
+ """Analyzes in vitro drug release kinetics from biomaterial formulations.
603
+
604
+ Parameters
605
+ ----------
606
+ time_points : list or numpy.ndarray
607
+ Time points at which drug concentrations were measured (in hours)
608
+ concentration_data : list or numpy.ndarray
609
+ Measured drug concentration at each time point
610
+ drug_name : str, optional
611
+ Name of the drug being analyzed (default: "Drug")
612
+ total_drug_loaded : float, optional
613
+ Total amount of drug initially loaded in the formulation.
614
+ If None, the maximum concentration is used as 100% (default: None)
615
+ output_dir : str, optional
616
+ Directory to save output files (default: "./")
617
+
618
+ Returns
619
+ -------
620
+ str
621
+ Research log summarizing the analysis steps, results, and saved file locations
622
+
623
+ """
624
+ import os
625
+ from datetime import datetime
626
+
627
+ import matplotlib.pyplot as plt
628
+ import numpy as np
629
+ import pandas as pd
630
+ from scipy.optimize import curve_fit
631
+
632
+ # Ensure output directory exists
633
+ os.makedirs(output_dir, exist_ok=True)
634
+
635
+ # Convert inputs to numpy arrays
636
+ time_points = np.array(time_points)
637
+ concentration_data = np.array(concentration_data)
638
+
639
+ # Calculate cumulative release percentage
640
+ if total_drug_loaded is None:
641
+ total_drug_loaded = np.max(concentration_data)
642
+
643
+ cumulative_release = (concentration_data / total_drug_loaded) * 100
644
+
645
+ # Create a DataFrame for easier manipulation
646
+ release_df = pd.DataFrame(
647
+ {
648
+ "Time (hours)": time_points,
649
+ "Concentration": concentration_data,
650
+ "Cumulative Release (%)": cumulative_release,
651
+ }
652
+ )
653
+
654
+ # Calculate release rate (simple approximation using differences)
655
+ release_df["Release Rate"] = np.gradient(release_df["Cumulative Release (%)"], release_df["Time (hours)"])
656
+
657
+ # Define kinetic models
658
+ def zero_order(t, k):
659
+ return k * t
660
+
661
+ def first_order(t, k):
662
+ return 100 * (1 - np.exp(-k * t))
663
+
664
+ def higuchi(t, k):
665
+ return k * np.sqrt(t)
666
+
667
+ def korsmeyer_peppas(t, k, n):
668
+ return 100 * (k * t) ** n
669
+
670
+ # Fit data to different kinetic models
671
+ models = {}
672
+ r2_values = {}
673
+
674
+ # Zero-order kinetics
675
+ try:
676
+ params, _ = curve_fit(zero_order, time_points, cumulative_release)
677
+ y_pred = zero_order(time_points, *params)
678
+ ss_total = np.sum((cumulative_release - np.mean(cumulative_release)) ** 2)
679
+ ss_residual = np.sum((cumulative_release - y_pred) ** 2)
680
+ r2 = 1 - (ss_residual / ss_total)
681
+ models["Zero-order"] = {
682
+ "params": params,
683
+ "equation": f"Release = {params[0]:.4f} * t",
684
+ "pred": y_pred,
685
+ }
686
+ r2_values["Zero-order"] = r2
687
+ except Exception:
688
+ models["Zero-order"] = {
689
+ "params": None,
690
+ "equation": "Fitting failed",
691
+ "pred": None,
692
+ }
693
+ r2_values["Zero-order"] = 0
694
+
695
+ # First-order kinetics
696
+ try:
697
+ params, _ = curve_fit(first_order, time_points, cumulative_release, bounds=(0, [1]))
698
+ y_pred = first_order(time_points, *params)
699
+ ss_total = np.sum((cumulative_release - np.mean(cumulative_release)) ** 2)
700
+ ss_residual = np.sum((cumulative_release - y_pred) ** 2)
701
+ r2 = 1 - (ss_residual / ss_total)
702
+ models["First-order"] = {
703
+ "params": params,
704
+ "equation": f"Release = 100 * (1 - exp(-{params[0]:.4f} * t))",
705
+ "pred": y_pred,
706
+ }
707
+ r2_values["First-order"] = r2
708
+ except Exception:
709
+ models["First-order"] = {
710
+ "params": None,
711
+ "equation": "Fitting failed",
712
+ "pred": None,
713
+ }
714
+ r2_values["First-order"] = 0
715
+
716
+ # Higuchi model
717
+ try:
718
+ params, _ = curve_fit(higuchi, time_points, cumulative_release)
719
+ y_pred = higuchi(time_points, *params)
720
+ ss_total = np.sum((cumulative_release - np.mean(cumulative_release)) ** 2)
721
+ ss_residual = np.sum((cumulative_release - y_pred) ** 2)
722
+ r2 = 1 - (ss_residual / ss_total)
723
+ models["Higuchi"] = {
724
+ "params": params,
725
+ "equation": f"Release = {params[0]:.4f} * sqrt(t)",
726
+ "pred": y_pred,
727
+ }
728
+ r2_values["Higuchi"] = r2
729
+ except Exception:
730
+ models["Higuchi"] = {"params": None, "equation": "Fitting failed", "pred": None}
731
+ r2_values["Higuchi"] = 0
732
+
733
+ # Korsmeyer-Peppas model
734
+ try:
735
+ # Only use the first 60% of release data for Korsmeyer-Peppas model
736
+ mask = cumulative_release <= 60
737
+ if sum(mask) >= 3: # Need at least 3 points for fitting
738
+ params, _ = curve_fit(
739
+ korsmeyer_peppas,
740
+ time_points[mask],
741
+ cumulative_release[mask],
742
+ bounds=([0, 0], [1, 1]),
743
+ )
744
+ y_pred = korsmeyer_peppas(time_points, *params)
745
+ ss_total = np.sum((cumulative_release - np.mean(cumulative_release)) ** 2)
746
+ ss_residual = np.sum((cumulative_release - y_pred) ** 2)
747
+ r2 = 1 - (ss_residual / ss_total)
748
+ models["Korsmeyer-Peppas"] = {
749
+ "params": params,
750
+ "equation": f"Release = 100 * ({params[0]:.4f} * t)^{params[1]:.4f}",
751
+ "pred": y_pred,
752
+ }
753
+ r2_values["Korsmeyer-Peppas"] = r2
754
+ else:
755
+ models["Korsmeyer-Peppas"] = {
756
+ "params": None,
757
+ "equation": "Insufficient data points",
758
+ "pred": None,
759
+ }
760
+ r2_values["Korsmeyer-Peppas"] = 0
761
+ except Exception:
762
+ models["Korsmeyer-Peppas"] = {
763
+ "params": None,
764
+ "equation": "Fitting failed",
765
+ "pred": None,
766
+ }
767
+ r2_values["Korsmeyer-Peppas"] = 0
768
+
769
+ # Determine best model based on R² value
770
+ best_model = max(r2_values, key=r2_values.get)
771
+
772
+ # Calculate half-life (time to 50% release)
773
+ try:
774
+ # Use best model to calculate half-life
775
+ if best_model == "Zero-order":
776
+ k = models[best_model]["params"][0]
777
+ half_life = 50 / k if k > 0 else float("inf")
778
+ elif best_model == "First-order":
779
+ k = models[best_model]["params"][0]
780
+ half_life = -np.log(0.5) / k if k > 0 else float("inf")
781
+ elif best_model == "Higuchi":
782
+ k = models[best_model]["params"][0]
783
+ half_life = (50 / k) ** 2 if k > 0 else float("inf")
784
+ elif best_model == "Korsmeyer-Peppas":
785
+ k, n = models[best_model]["params"]
786
+ half_life = (0.5 ** (1 / n)) / k if k > 0 else float("inf")
787
+ else:
788
+ # Interpolate from data if model fitting failed
789
+ from scipy.interpolate import interp1d
790
+
791
+ if np.max(cumulative_release) >= 50:
792
+ f = interp1d(cumulative_release, time_points)
793
+ half_life = float(f(50))
794
+ else:
795
+ half_life = "Not reached"
796
+ except Exception:
797
+ half_life = "Could not calculate"
798
+
799
+ # Create plots
800
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
801
+
802
+ # 1. Cumulative release plot with model fits
803
+ plt.figure(figsize=(10, 6))
804
+ plt.plot(time_points, cumulative_release, "o-", label="Experimental data")
805
+
806
+ for model_name, model_data in models.items():
807
+ if model_data["pred"] is not None:
808
+ plt.plot(
809
+ time_points,
810
+ model_data["pred"],
811
+ "--",
812
+ label=f"{model_name} (R² = {r2_values[model_name]:.4f})",
813
+ )
814
+
815
+ plt.xlabel("Time (hours)")
816
+ plt.ylabel("Cumulative Release (%)")
817
+ plt.title(f"In Vitro Release Profile of {drug_name}")
818
+ plt.legend()
819
+ plt.grid(True, linestyle="--", alpha=0.7)
820
+ cumulative_plot_path = os.path.join(output_dir, f"cumulative_release_{timestamp}.png")
821
+ plt.savefig(cumulative_plot_path, dpi=300, bbox_inches="tight")
822
+ plt.close()
823
+
824
+ # 2. Release rate plot
825
+ plt.figure(figsize=(10, 6))
826
+ plt.plot(time_points, release_df["Release Rate"], "o-")
827
+ plt.xlabel("Time (hours)")
828
+ plt.ylabel("Release Rate (%/hour)")
829
+ plt.title(f"Release Rate of {drug_name}")
830
+ plt.grid(True, linestyle="--", alpha=0.7)
831
+ rate_plot_path = os.path.join(output_dir, f"release_rate_{timestamp}.png")
832
+ plt.savefig(rate_plot_path, dpi=300, bbox_inches="tight")
833
+ plt.close()
834
+
835
+ # Save data to CSV
836
+ csv_path = os.path.join(output_dir, f"drug_release_data_{timestamp}.csv")
837
+ release_df.to_csv(csv_path, index=False)
838
+
839
+ # Generate research log
840
+ log = f"""
841
+ # In Vitro Drug Release Kinetics Analysis for {drug_name}
842
+
843
+ ## Analysis Summary
844
+ - **Date/Time:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
845
+ - **Drug Analyzed:** {drug_name}
846
+ - **Time Range:** {min(time_points)} to {max(time_points)} hours
847
+ - **Number of Data Points:** {len(time_points)}
848
+ - **Maximum Release Achieved:** {max(cumulative_release):.2f}%
849
+
850
+ ## Kinetic Models Analysis
851
+ The release data was fitted to four standard kinetic models:
852
+
853
+ 1. **Zero-order Model:** {models["Zero-order"]["equation"]} (R² = {r2_values["Zero-order"]:.4f})
854
+ 2. **First-order Model:** {models["First-order"]["equation"]} (R² = {r2_values["First-order"]:.4f})
855
+ 3. **Higuchi Model:** {models["Higuchi"]["equation"]} (R² = {r2_values["Higuchi"]:.4f})
856
+ 4. **Korsmeyer-Peppas Model:** {models["Korsmeyer-Peppas"]["equation"]} (R² = {r2_values["Korsmeyer-Peppas"]:.4f})
857
+
858
+ **Best-fitting Model:** {best_model} (R² = {r2_values[best_model]:.4f})
859
+
860
+ ## Release Metrics
861
+ - **Half-life (t50%):** {half_life if isinstance(half_life, str) else f"{half_life:.2f} hours"}
862
+ - **Initial Release Rate:** {release_df["Release Rate"].iloc[0]:.4f} %/hour
863
+ - **Average Release Rate:** {np.mean(release_df["Release Rate"]):.4f} %/hour
864
+
865
+ ## Files Generated
866
+ 1. Cumulative Release Plot: {cumulative_plot_path}
867
+ 2. Release Rate Plot: {rate_plot_path}
868
+ 3. Data CSV: {csv_path}
869
+
870
+ ## Interpretation
871
+ The drug release profile of {drug_name} best follows a {
872
+ best_model
873
+ } kinetic model, which suggests that the release mechanism is primarily driven by {
874
+ "diffusion through a porous matrix"
875
+ if best_model == "Higuchi"
876
+ else "diffusion with erosion"
877
+ if best_model == "Korsmeyer-Peppas" and 0.43 <= models[best_model]["params"][1] <= 0.85
878
+ else "Fickian diffusion"
879
+ if best_model == "Korsmeyer-Peppas" and models[best_model]["params"][1] < 0.43
880
+ else "case-II transport"
881
+ if best_model == "Korsmeyer-Peppas" and models[best_model]["params"][1] > 0.85
882
+ else "concentration-dependent diffusion"
883
+ if best_model == "First-order"
884
+ else "constant release rate independent of concentration"
885
+ if best_model == "Zero-order"
886
+ else "complex mechanisms"
887
+ }.
888
+ """
889
+
890
+ return log.strip()
891
+
892
+
893
+ def analyze_myofiber_morphology(
894
+ image_path,
895
+ nuclei_channel=2,
896
+ myofiber_channel=1,
897
+ threshold_method="otsu",
898
+ output_dir="./",
899
+ ):
900
+ """Quantifies morphological properties of myofibers in microscopy images of tissue sections.
901
+
902
+ Parameters
903
+ ----------
904
+ image_path : str
905
+ Path to the microscopy image file (typically a multichannel image with nuclei and myofiber staining)
906
+ nuclei_channel : int, default=2
907
+ Channel index containing nuclei staining (DAPI, Hoechst, etc.)
908
+ myofiber_channel : int, default=1
909
+ Channel index containing myofiber staining (α-Actinin, etc.)
910
+ threshold_method : str, default='otsu'
911
+ Method for thresholding ('otsu', 'adaptive', or 'manual')
912
+ output_dir : str, default='./'
913
+ Directory to save output files
914
+
915
+ Returns
916
+ -------
917
+ str
918
+ Research log summarizing the analysis steps and results
919
+
920
+ """
921
+ import os
922
+ from datetime import datetime
923
+
924
+ import numpy as np
925
+ import pandas as pd
926
+ from skimage import exposure, filters, io, measure, morphology
927
+ from skimage.color import label2rgb
928
+
929
+ # Create output directory if it doesn't exist
930
+ os.makedirs(output_dir, exist_ok=True)
931
+
932
+ # Load the image
933
+ image = io.imread(image_path)
934
+
935
+ # Extract channels (assuming multichannel image)
936
+ if len(image.shape) > 2:
937
+ if len(image.shape) == 3:
938
+ # RGB image
939
+ nuclei_img = image[:, :, nuclei_channel] if nuclei_channel < image.shape[2] else image[:, :, 0]
940
+ myofiber_img = image[:, :, myofiber_channel] if myofiber_channel < image.shape[2] else image[:, :, 1]
941
+ else:
942
+ # Multichannel image (e.g., from confocal)
943
+ nuclei_img = image[nuclei_channel, :, :] if nuclei_channel < image.shape[0] else image[0, :, :]
944
+ myofiber_img = image[myofiber_channel, :, :] if myofiber_channel < image.shape[0] else image[1, :, :]
945
+ else:
946
+ # Single channel image - can't separate nuclei and myofibers
947
+ return "Error: Input image must be multichannel to separate nuclei and myofibers"
948
+
949
+ # Enhance contrast
950
+ nuclei_img = exposure.equalize_adapthist(nuclei_img)
951
+ myofiber_img = exposure.equalize_adapthist(myofiber_img)
952
+
953
+ # Segment nuclei
954
+ if threshold_method == "otsu":
955
+ nuclei_thresh = filters.threshold_otsu(nuclei_img)
956
+ elif threshold_method == "adaptive":
957
+ nuclei_thresh = filters.threshold_local(nuclei_img, block_size=35)
958
+ else: # manual
959
+ nuclei_thresh = np.mean(nuclei_img) * 1.5
960
+
961
+ nuclei_binary = nuclei_img > nuclei_thresh
962
+ nuclei_binary = morphology.remove_small_objects(nuclei_binary, min_size=30)
963
+ nuclei_binary = morphology.binary_closing(nuclei_binary)
964
+
965
+ # Label nuclei
966
+ nuclei_labels = measure.label(nuclei_binary)
967
+ nuclei_props = measure.regionprops(nuclei_labels)
968
+
969
+ # Segment myofibers
970
+ if threshold_method == "otsu":
971
+ myofiber_thresh = filters.threshold_otsu(myofiber_img)
972
+ elif threshold_method == "adaptive":
973
+ myofiber_thresh = filters.threshold_local(myofiber_img, block_size=101)
974
+ else: # manual
975
+ myofiber_thresh = np.mean(myofiber_img) * 1.2
976
+
977
+ myofiber_binary = myofiber_img > myofiber_thresh
978
+ myofiber_binary = morphology.remove_small_objects(myofiber_binary, min_size=500)
979
+ myofiber_binary = morphology.binary_closing(myofiber_binary, morphology.disk(3))
980
+
981
+ # Label myofibers
982
+ myofiber_labels = measure.label(myofiber_binary)
983
+ myofiber_props = measure.regionprops(myofiber_labels)
984
+
985
+ # Count nuclei inside myofibers
986
+ nuclei_inside = 0
987
+ nuclei_total = len(nuclei_props)
988
+
989
+ for nucleus in nuclei_props:
990
+ y, x = nucleus.centroid
991
+ y, x = int(y), int(x)
992
+ if myofiber_binary[y, x]:
993
+ nuclei_inside += 1
994
+
995
+ percent_inside = nuclei_inside / nuclei_total * 100 if nuclei_total > 0 else 0
996
+
997
+ # Calculate myofiber morphological properties
998
+ myofiber_data = []
999
+ for fiber in myofiber_props:
1000
+ myofiber_data.append(
1001
+ {
1002
+ "Area": fiber.area,
1003
+ "Perimeter": fiber.perimeter,
1004
+ "Eccentricity": fiber.eccentricity,
1005
+ "Solidity": fiber.solidity,
1006
+ "Orientation": fiber.orientation,
1007
+ }
1008
+ )
1009
+
1010
+ # Save results
1011
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1012
+ results_file = f"{output_dir}/myofiber_analysis_{timestamp}.csv"
1013
+
1014
+ if myofiber_data:
1015
+ df = pd.DataFrame(myofiber_data)
1016
+ df.to_csv(results_file, index=False)
1017
+
1018
+ # Calculate summary statistics
1019
+ mean_area = df["Area"].mean()
1020
+ mean_perimeter = df["Perimeter"].mean()
1021
+ mean_eccentricity = df["Eccentricity"].mean()
1022
+ else:
1023
+ mean_area = mean_perimeter = mean_eccentricity = 0
1024
+
1025
+ # Save labeled image
1026
+ labeled_image = label2rgb(myofiber_labels, image=myofiber_img)
1027
+ labeled_image_path = f"{output_dir}/labeled_myofibers_{timestamp}.png"
1028
+ io.imsave(labeled_image_path, (labeled_image * 255).astype(np.uint8))
1029
+
1030
+ # Create research log
1031
+ log = f"""
1032
+ MYOFIBER MORPHOLOGICAL ANALYSIS REPORT
1033
+ ======================================
1034
+ Date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
1035
+ Image: {image_path}
1036
+
1037
+ ANALYSIS STEPS:
1038
+ 1. Loaded multichannel microscopy image
1039
+ 2. Extracted nuclei (channel {nuclei_channel}) and myofiber (channel {myofiber_channel}) signals
1040
+ 3. Enhanced contrast using adaptive histogram equalization
1041
+ 4. Segmented nuclei using {threshold_method} thresholding
1042
+ 5. Segmented myofibers using {threshold_method} thresholding
1043
+ 6. Performed morphological operations to refine segmentation
1044
+ 7. Identified and measured individual myofibers and nuclei
1045
+
1046
+ RESULTS:
1047
+ - Total myofibers detected: {len(myofiber_props)}
1048
+ - Total nuclei detected: {nuclei_total}
1049
+ - Nuclei inside myofibers: {nuclei_inside} ({percent_inside:.2f}%)
1050
+ - Mean myofiber area: {mean_area:.2f} pixels
1051
+ - Mean myofiber perimeter: {mean_perimeter:.2f} pixels
1052
+ - Mean myofiber eccentricity: {mean_eccentricity:.2f}
1053
+
1054
+ FILES GENERATED:
1055
+ - Morphological measurements: {results_file}
1056
+ - Labeled myofiber image: {labeled_image_path}
1057
+ """
1058
+
1059
+ return log
1060
+
1061
+
1062
+ def decode_behavior_from_neural_trajectories(neural_data, behavioral_data, n_components=10, output_dir="./"):
1063
+ """Model neural activity trajectories and decode behavioral variables.
1064
+
1065
+ Parameters
1066
+ ----------
1067
+ neural_data : numpy.ndarray
1068
+ Neural spiking activity data, shape (n_timepoints, n_neurons)
1069
+ behavioral_data : numpy.ndarray
1070
+ Behavioral data, shape (n_timepoints, n_behavioral_variables)
1071
+ n_components : int, optional
1072
+ Number of principal components to use for dimensionality reduction, default is 10
1073
+ output_dir : str, optional
1074
+ Directory to save output files, default is "./"
1075
+
1076
+ Returns
1077
+ -------
1078
+ str
1079
+ Research log summarizing the steps taken and results
1080
+
1081
+ """
1082
+ import os
1083
+ import pickle
1084
+
1085
+ import matplotlib.pyplot as plt
1086
+ import numpy as np
1087
+ from pykalman import KalmanFilter
1088
+ from sklearn.decomposition import PCA
1089
+ from sklearn.metrics import mean_squared_error
1090
+ from sklearn.model_selection import train_test_split
1091
+
1092
+ # Create output directory if it doesn't exist
1093
+ if not os.path.exists(output_dir):
1094
+ os.makedirs(output_dir)
1095
+
1096
+ # Initialize research log
1097
+ log = "# Neural Trajectory Modeling and Decoding Research Log\n\n"
1098
+
1099
+ # Step 1: Preprocess the data
1100
+ log += "## Step 1: Data Preprocessing\n"
1101
+ log += f"- Neural data shape: {neural_data.shape}\n"
1102
+ log += f"- Behavioral data shape: {behavioral_data.shape}\n"
1103
+
1104
+ # Check for NaN values and replace with zeros
1105
+ neural_data = np.nan_to_num(neural_data)
1106
+ behavioral_data = np.nan_to_num(behavioral_data)
1107
+
1108
+ # Split data into training and testing sets
1109
+ X_train, X_test, y_train, y_test = train_test_split(neural_data, behavioral_data, test_size=0.2, random_state=42)
1110
+ log += f"- Training set size: {X_train.shape[0]} samples\n"
1111
+ log += f"- Testing set size: {X_test.shape[0]} samples\n\n"
1112
+
1113
+ # Step 2: Dimensionality reduction with PCA
1114
+ log += "## Step 2: Dimensionality Reduction\n"
1115
+ log += f"- Reducing neural data from {neural_data.shape[1]} dimensions to {n_components} components\n"
1116
+
1117
+ pca = PCA(n_components=n_components)
1118
+ X_train_pca = pca.fit_transform(X_train)
1119
+ X_test_pca = pca.transform(X_test)
1120
+
1121
+ explained_variance = np.sum(pca.explained_variance_ratio_) * 100
1122
+ log += f"- Total variance explained: {explained_variance:.2f}%\n\n"
1123
+
1124
+ # Save PCA components visualization
1125
+ try:
1126
+ plt.figure(figsize=(10, 6))
1127
+ plt.bar(range(1, n_components + 1), pca.explained_variance_ratio_)
1128
+ plt.xlabel("Principal Component")
1129
+ plt.ylabel("Explained Variance Ratio")
1130
+ plt.title("PCA Components Explained Variance")
1131
+ plt.xticks(range(1, n_components + 1))
1132
+ plt.tight_layout()
1133
+
1134
+ pca_plot_path = os.path.join(output_dir, "pca_explained_variance.png")
1135
+ plt.savefig(pca_plot_path, dpi=300)
1136
+ plt.close()
1137
+
1138
+ log += f"- PCA components visualization saved to: {pca_plot_path}\n\n"
1139
+ except Exception as e:
1140
+ log += f"- Error creating PCA visualization: {str(e)}\n\n"
1141
+
1142
+ # Step 3: Train a Kalman filter for decoding
1143
+ log += "## Step 3: Trajectory Modeling and Decoding\n"
1144
+ log += "- Training Kalman filter to decode behavioral variables from neural trajectories\n"
1145
+
1146
+ # Initialize and train Kalman filter
1147
+ kf = KalmanFilter(initial_state_mean=np.zeros(y_train.shape[1]), n_dim_obs=X_train_pca.shape[1])
1148
+
1149
+ # Fit the Kalman filter to the data
1150
+ kf.em(X_train_pca, y_train)
1151
+
1152
+ # Step 4: Decode behavioral variables
1153
+ log += "## Step 4: Decoding Behavioral Variables\n"
1154
+
1155
+ # Use the Kalman filter to predict behavioral variables
1156
+ y_pred, _ = kf.filter(X_test_pca)
1157
+
1158
+ # Evaluate performance
1159
+ mse = mean_squared_error(y_test, y_pred)
1160
+ log += f"- Mean squared error on test set: {mse:.4f}\n\n"
1161
+
1162
+ # Save the decoded trajectories visualization
1163
+ try:
1164
+ if y_test.shape[1] >= 2:
1165
+ # Create visualization of true vs. predicted trajectories (first 2 dimensions)
1166
+ plt.figure(figsize=(12, 6))
1167
+
1168
+ # First behavioral variable
1169
+ plt.subplot(1, 2, 1)
1170
+ plt.plot(y_test[:, 0], label="True")
1171
+ plt.plot(y_pred[:, 0], label="Predicted")
1172
+ plt.xlabel("Time steps")
1173
+ plt.ylabel("Behavioral Variable 1")
1174
+ plt.title("Decoding Performance - Variable 1")
1175
+ plt.legend()
1176
+
1177
+ # Second behavioral variable
1178
+ plt.subplot(1, 2, 2)
1179
+ plt.plot(y_test[:, 1], label="True")
1180
+ plt.plot(y_pred[:, 1], label="Predicted")
1181
+ plt.xlabel("Time steps")
1182
+ plt.ylabel("Behavioral Variable 2")
1183
+ plt.title("Decoding Performance - Variable 2")
1184
+ plt.legend()
1185
+
1186
+ plt.tight_layout()
1187
+
1188
+ trajectory_plot_path = os.path.join(output_dir, "decoded_trajectories.png")
1189
+ plt.savefig(trajectory_plot_path, dpi=300)
1190
+ plt.close()
1191
+
1192
+ log += f"- Decoded trajectories visualization saved to: {trajectory_plot_path}\n"
1193
+ except Exception as e:
1194
+ log += f"- Error creating trajectory visualization: {str(e)}\n"
1195
+
1196
+ # Save the results as a pickle file
1197
+ results = {
1198
+ "true_behavior": y_test,
1199
+ "predicted_behavior": y_pred,
1200
+ "pca_model": pca,
1201
+ "kalman_filter": kf,
1202
+ "mse": mse,
1203
+ }
1204
+
1205
+ results_file = os.path.join(output_dir, "neural_decoding_results.pkl")
1206
+ with open(results_file, "wb") as f:
1207
+ pickle.dump(results, f)
1208
+
1209
+ # Also save a CSV with the first few predicted vs. actual values for easier inspection
1210
+ try:
1211
+ import pandas as pd
1212
+
1213
+ n_samples = min(100, y_test.shape[0])
1214
+ n_vars = y_test.shape[1]
1215
+
1216
+ results_data = {}
1217
+ for i in range(n_vars):
1218
+ results_data[f"true_var{i + 1}"] = y_test[:n_samples, i]
1219
+ results_data[f"pred_var{i + 1}"] = y_pred[:n_samples, i]
1220
+
1221
+ results_df = pd.DataFrame(results_data)
1222
+ csv_path = os.path.join(output_dir, "decoding_results_sample.csv")
1223
+ results_df.to_csv(csv_path, index=False)
1224
+
1225
+ log += f"- Sample of decoding results saved to: {csv_path}\n"
1226
+ except Exception as e:
1227
+ log += f"- Error creating CSV results: {str(e)}\n"
1228
+
1229
+ log += "\n## Results\n"
1230
+ log += f"- Full decoded behavioral trajectories saved to: {results_file}\n"
1231
+ log += f"- Decoder performance (MSE): {mse:.4f}\n"
1232
+
1233
+ # Save the log to a file
1234
+ log_file = os.path.join(output_dir, "neural_decoding_log.txt")
1235
+ with open(log_file, "w") as f:
1236
+ f.write(log)
1237
+
1238
+ log += f"- Analysis log saved to: {log_file}\n"
1239
+
1240
+ return log
1241
+
1242
+
1243
+ def simulate_whole_cell_ode_model(
1244
+ initial_conditions,
1245
+ parameters,
1246
+ ode_function=None,
1247
+ time_span=(0, 100),
1248
+ time_points=1000,
1249
+ method="LSODA",
1250
+ ):
1251
+ """Simulate a whole-cell model represented as a system of ordinary differential equations (ODEs).
1252
+
1253
+ Parameters
1254
+ ----------
1255
+ initial_conditions : dict or array-like
1256
+ Initial values for each state variable in the model. If dict, keys are variable names
1257
+ and values are initial concentrations/values. If array-like, order must match the
1258
+ order expected by the ODE function.
1259
+ parameters : dict
1260
+ Model parameters required by the ODE function. Keys are parameter names and
1261
+ values are parameter values.
1262
+ ode_function : callable, optional
1263
+ Function defining the system of ODEs. Should take arguments ``(t, y, *args)`` where
1264
+ ``t`` is time, ``y`` is the state vector, and ``args`` contains additional parameters.
1265
+ If None, a simple example whole-cell model will be used.
1266
+ time_span : tuple, default=(0, 100)
1267
+ Tuple of (start_time, end_time) for the simulation.
1268
+ time_points : int, default=1000
1269
+ Number of time points to evaluate.
1270
+ method : str, default='LSODA'
1271
+ Numerical integration method to use (e.g., 'RK45', 'LSODA', 'BDF').
1272
+
1273
+ Returns
1274
+ -------
1275
+ str
1276
+ Research log summarizing the simulation steps and results. Results are saved
1277
+ to a CSV file and the filename is included in the log.
1278
+
1279
+ """
1280
+ from datetime import datetime
1281
+
1282
+ import numpy as np
1283
+ import pandas as pd
1284
+ from scipy.integrate import solve_ivp
1285
+
1286
+ # Define a default ODE function if none is provided
1287
+ if ode_function is None:
1288
+
1289
+ def default_whole_cell_model(t, y, params):
1290
+ # Unpack state variables
1291
+ # Simple model with:
1292
+ # - mRNA (y[0])
1293
+ # - Protein (y[1])
1294
+ # - Metabolite (y[2])
1295
+ # - ATP (y[3])
1296
+ mRNA, protein, metabolite, atp = y
1297
+
1298
+ # Unpack parameters
1299
+ k_transcription = params["k_transcription"] # mRNA synthesis rate
1300
+ k_translation = params["k_translation"] # Protein synthesis rate
1301
+ k_mrna_deg = params["k_mrna_deg"] # mRNA degradation rate
1302
+ k_protein_deg = params["k_protein_deg"] # Protein degradation rate
1303
+ k_metabolism = params["k_metabolism"] # Metabolite production rate
1304
+ k_atp_production = params["k_atp_production"] # ATP production rate
1305
+ k_atp_consumption = params["k_atp_consumption"] # ATP consumption rate
1306
+
1307
+ # ODEs
1308
+ dmRNA_dt = k_transcription - k_mrna_deg * mRNA
1309
+ dprotein_dt = k_translation * mRNA * atp - k_protein_deg * protein
1310
+ dmetabolite_dt = k_metabolism * protein - k_atp_production * metabolite
1311
+ datp_dt = k_atp_production * metabolite - k_atp_consumption * atp - k_translation * mRNA * atp
1312
+
1313
+ return [dmRNA_dt, dprotein_dt, dmetabolite_dt, datp_dt]
1314
+
1315
+ ode_function = default_whole_cell_model
1316
+
1317
+ # Prepare initial conditions as array
1318
+ if isinstance(initial_conditions, dict):
1319
+ y0_values = list(initial_conditions.values())
1320
+ variable_names = list(initial_conditions.keys())
1321
+ else:
1322
+ y0_values = initial_conditions
1323
+ variable_names = [f"Variable_{i}" for i in range(len(initial_conditions))]
1324
+
1325
+ # Set up time points
1326
+ t_eval = np.linspace(time_span[0], time_span[1], time_points)
1327
+
1328
+ # Start research log
1329
+ log = []
1330
+ log.append("# Whole-Cell ODE Model Simulation")
1331
+ log.append(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
1332
+ log.append("\n## Simulation Setup")
1333
+ log.append(f"- Integration method: {method}")
1334
+ log.append(f"- Time span: {time_span[0]} to {time_span[1]} time units")
1335
+ log.append(f"- Number of time points: {time_points}")
1336
+ log.append(f"- Number of state variables: {len(y0_values)}")
1337
+ log.append("\n## Initial Conditions")
1338
+ for _i, (name, value) in enumerate(zip(variable_names, y0_values, strict=False)):
1339
+ log.append(f"- {name}: {value}")
1340
+
1341
+ log.append("\n## Model Parameters")
1342
+ for param, value in parameters.items():
1343
+ log.append(f"- {param}: {value}")
1344
+
1345
+ # Solve the ODE system
1346
+ log.append("\n## Running Simulation")
1347
+ try:
1348
+ solution = solve_ivp(
1349
+ lambda t, y: ode_function(t, y, parameters),
1350
+ time_span,
1351
+ y0_values,
1352
+ method=method,
1353
+ t_eval=t_eval,
1354
+ )
1355
+
1356
+ # Check if simulation was successful
1357
+ if solution.success:
1358
+ log.append("Simulation completed successfully.")
1359
+ log.append(f"- Number of function evaluations: {solution.nfev}")
1360
+ log.append(f"- Number of Jacobian evaluations: {solution.njev}")
1361
+ log.append(f"- Number of steps: {len(solution.t)}")
1362
+
1363
+ # Create DataFrame with results
1364
+ results_df = pd.DataFrame(solution.y.T, columns=variable_names)
1365
+ results_df.insert(0, "Time", solution.t)
1366
+
1367
+ # Save results to CSV
1368
+ filename = f"whole_cell_simulation_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
1369
+ results_df.to_csv(filename, index=False)
1370
+
1371
+ log.append("\n## Results Summary")
1372
+ log.append(f"Simulation results saved to: {filename}")
1373
+
1374
+ # Calculate some basic statistics
1375
+ final_state = results_df.iloc[-1].drop("Time").to_dict()
1376
+ log.append("\n## Final State")
1377
+ for var, value in final_state.items():
1378
+ log.append(f"- {var}: {value:.6f}")
1379
+
1380
+ else:
1381
+ log.append(f"Simulation failed with message: {solution.message}")
1382
+
1383
+ except Exception as e:
1384
+ log.append(f"Error during simulation: {str(e)}")
1385
+
1386
+ # Return the research log
1387
+ return "\n".join(log)
Biomanus_upload/biomni/tool/bioimaging.py ADDED
@@ -0,0 +1,1394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import zipfile
4
+
5
+ import matplotlib
6
+ import requests
7
+
8
+ matplotlib.use("Agg") # Use non-interactive backend
9
+ import nibabel as nib
10
+ import numpy as np
11
+ import SimpleITK as sitk
12
+ import torch
13
+ import torch.serialization
14
+ from nnunet.inference.predict import predict_from_folder
15
+
16
+ # Apply safe globals for torch serialization
17
+ torch.serialization.add_safe_globals([tuple, list, dict, set, int, float, str, bytes, bytearray])
18
+ torch.serialization.add_safe_globals([complex, slice, range])
19
+ torch.serialization.add_safe_globals([np.core.multiarray.scalar])
20
+
21
+ # Configure logging
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ # ============================================================================
26
+ # SEGMENTATION CLASS
27
+ # ============================================================================
28
+
29
+
30
+ class SegmentationTool:
31
+ """
32
+ A comprehensive tool for medical image segmentation using nnUNet.
33
+ Handles BRATS dataset processing, modality splitting, and segmentation visualization.
34
+ """
35
+
36
+ def __init__(self):
37
+ """Initialize the SegmentationTool."""
38
+ self.supported_formats = [".nii", ".nii.gz"]
39
+ logger.info("SegmentationTool initialized")
40
+
41
+ def split_modalities(self, input_file, output_dir, case_name="BRAT"):
42
+ """
43
+ Split a 4D NIfTI file into separate modality files for nnUNet
44
+ Args:
45
+ input_file: Path to the 4D NIfTI file
46
+ output_dir: Directory to save the split files
47
+ case_name: Base name for the case (default: BRAT)
48
+ Returns:
49
+ output_dir: Path to directory containing split modality files
50
+ """
51
+ os.makedirs(output_dir, exist_ok=True)
52
+
53
+ # Load the 4D image
54
+ print(f"Loading {input_file}...")
55
+ img = nib.load(input_file)
56
+ data = img.get_fdata()
57
+
58
+ print(f"Image shape: {data.shape}")
59
+ print("Expected shape: (X, Y, Z, 4) for 4 modalities")
60
+
61
+ if len(data.shape) != 4:
62
+ raise ValueError(f"Expected 4D image, got {len(data.shape)}D")
63
+
64
+ if data.shape[3] != 4:
65
+ raise ValueError(f"Expected 4 modalities, got {data.shape[3]}")
66
+
67
+ # Split into separate files
68
+ modalities = ["FLAIR", "T1w", "t1gd", "T2w"]
69
+
70
+ for i, modality in enumerate(modalities):
71
+ # Extract the modality data
72
+ modality_data = data[:, :, :, i]
73
+
74
+ # Create a new NIfTI image with the same header but 3D data
75
+ modality_img = nib.Nifti1Image(modality_data, img.affine, img.header)
76
+
77
+ # Save with the expected naming convention
78
+ output_file = os.path.join(output_dir, f"{case_name}_{i:04d}.nii.gz")
79
+ nib.save(modality_img, output_file)
80
+
81
+ print(f"Saved {modality} modality to {output_file}")
82
+ print(f" Shape: {modality_data.shape}, Data type: {modality_data.dtype}")
83
+
84
+ print(f"\nAll modalities saved to {output_dir}")
85
+ return output_dir
86
+
87
+ def prepare_input_for_nnunet(self, input_path, output_dir, case_name="BRAT"):
88
+ """
89
+ Prepare input data for nnUNet by handling both 4D and pre-split modality files
90
+ Args:
91
+ input_path: Path to input file or directory
92
+ output_dir: Directory to save prepared files
93
+ case_name: Base name for the case (default: BRAT)
94
+ Returns:
95
+ prepared_dir: Path to directory with nnUNet-ready files
96
+ """
97
+ os.makedirs(output_dir, exist_ok=True)
98
+
99
+ if os.path.isfile(input_path):
100
+ # Single file - check if it's 4D
101
+ if input_path.endswith((".nii", ".nii.gz")):
102
+ try:
103
+ img = nib.load(input_path)
104
+ if len(img.shape) == 4 and img.shape[3] == 4:
105
+ print("4D NIfTI file detected, splitting modalities...")
106
+ return self.split_modalities(input_path, output_dir, case_name)
107
+ else:
108
+ print("Single 3D file detected, copying to output directory...")
109
+ # Copy single file with proper naming
110
+ output_file = os.path.join(output_dir, f"{case_name}_0000.nii.gz")
111
+ import shutil
112
+
113
+ shutil.copy2(input_path, output_file)
114
+ return output_dir
115
+ except Exception as e:
116
+ print(f"Error reading file {input_path}: {e}")
117
+ raise
118
+ elif os.path.isdir(input_path):
119
+ # Directory - check if it already has split modalities
120
+ files = [f for f in os.listdir(input_path) if f.endswith((".nii", ".nii.gz"))]
121
+
122
+ if any(f.endswith("_0000.nii.gz") for f in files):
123
+ print("Directory already contains split modality files, using as-is...")
124
+ # Copy existing files to output directory
125
+ for f in files:
126
+ if f.endswith((".nii", ".nii.gz")):
127
+ import shutil
128
+
129
+ shutil.copy2(os.path.join(input_path, f), os.path.join(output_dir, f))
130
+ return output_dir
131
+ else:
132
+ # Check if there's a 4D file to split
133
+ for f in files:
134
+ if f.endswith((".nii", ".nii.gz")):
135
+ try:
136
+ img = nib.load(os.path.join(input_path, f))
137
+ if len(img.shape) == 4 and img.shape[3] == 4:
138
+ print(f"4D NIfTI file {f} detected, splitting modalities...")
139
+ return self.split_modalities(os.path.join(input_path, f), output_dir, case_name)
140
+ except Exception as e:
141
+ logging.debug("Skipping file %s during 4D check: %s", f, e)
142
+ continue
143
+
144
+ print("No 4D files found, copying existing files...")
145
+ # Copy existing files to output directory
146
+ for f in files:
147
+ if f.endswith((".nii", ".nii.gz")):
148
+ import shutil
149
+
150
+ shutil.copy2(os.path.join(input_path, f), os.path.join(output_dir, f))
151
+ return output_dir
152
+
153
+ raise ValueError(f"Input path {input_path} is neither a valid file nor directory")
154
+
155
+ def setup_nnunet_environment(self, results_folder=None, raw_data_base=None, preprocessed=None):
156
+ """
157
+ Setup nnU-Net environment variables according to official documentation
158
+ Args:
159
+ results_folder: Path to nnUNet results folder (default: ~/nnUNet_results)
160
+ raw_data_base: Path to raw data base (default: ~/nnUNet_raw_data_base)
161
+ preprocessed: Path to preprocessed data (default: ~/nnUNet_preprocessed)
162
+ """
163
+ # Set nnUNet environment variables as per official documentation
164
+ if results_folder:
165
+ os.environ["nnUNet_RESULTS_FOLDER"] = os.path.expanduser(results_folder)
166
+ elif "nnUNet_RESULTS_FOLDER" not in os.environ:
167
+ os.environ["nnUNet_RESULTS_FOLDER"] = os.path.expanduser("~/nnUNet_results")
168
+
169
+ if raw_data_base:
170
+ os.environ["nnUNet_raw_data_base"] = os.path.expanduser(raw_data_base)
171
+ elif "nnUNet_raw_data_base" not in os.environ:
172
+ os.environ["nnUNet_raw_data_base"] = os.path.expanduser("~/nnUNet_raw_data_base")
173
+
174
+ if preprocessed:
175
+ os.environ["nnUNet_preprocessed"] = os.path.expanduser(preprocessed)
176
+ elif "nnUNet_preprocessed" not in os.environ:
177
+ os.environ["nnUNet_preprocessed"] = os.path.expanduser("~/nnUNet_preprocessed")
178
+
179
+ # Create directories if they don't exist
180
+ for path in [
181
+ os.environ["nnUNet_RESULTS_FOLDER"],
182
+ os.environ["nnUNet_raw_data_base"],
183
+ os.environ["nnUNet_preprocessed"],
184
+ ]:
185
+ os.makedirs(path, exist_ok=True)
186
+
187
+ print("nnU-Net environment variables set:")
188
+ print(f" nnUNet_RESULTS_FOLDER: {os.environ['nnUNet_RESULTS_FOLDER']}")
189
+ print(f" nnUNet_raw_data_base: {os.environ['nnUNet_raw_data_base']}")
190
+ print(f" nnUNet_preprocessed: {os.environ['nnUNet_preprocessed']}")
191
+
192
+ def _download_model_with_browser_headers(self, url, output_path):
193
+ """
194
+ Download model with browser-like headers to bypass Zenodo's anti-bot protection
195
+ """
196
+ headers = {
197
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
198
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
199
+ "Accept-Language": "en-US,en;q=0.5",
200
+ "Accept-Encoding": "gzip, deflate",
201
+ "Connection": "keep-alive",
202
+ "Upgrade-Insecure-Requests": "1",
203
+ }
204
+
205
+ logger.info(f"Downloading model from: {url}")
206
+
207
+ try:
208
+ response = requests.get(url, headers=headers, stream=True, timeout=300)
209
+ response.raise_for_status()
210
+
211
+ with open(output_path, "wb") as f:
212
+ for chunk in response.iter_content(chunk_size=8192):
213
+ if chunk:
214
+ f.write(chunk)
215
+
216
+ logger.info(f"Download completed: {output_path}")
217
+ return True
218
+
219
+ except Exception as e:
220
+ logger.error(f"Download failed: {e}")
221
+ if os.path.exists(output_path):
222
+ os.remove(output_path)
223
+ return False
224
+
225
+ def _download_and_extract_model(self, task_id, model_type="3d_fullres"):
226
+ """
227
+ Download and extract nnUNet model with browser-like headers - extracts directly to nnUNet directory
228
+ """
229
+ results_folder = os.environ.get("nnUNet_RESULTS_FOLDER", "~/nnUNet_results")
230
+ results_folder = os.path.expanduser(results_folder)
231
+
232
+ # Create the nnUNet directory
233
+ nnunet_dir = os.path.join(results_folder, "nnUNet")
234
+ os.makedirs(nnunet_dir, exist_ok=True)
235
+
236
+ # Check if model already exists
237
+ task_dir = os.path.join(nnunet_dir, model_type, task_id)
238
+ if os.path.exists(task_dir):
239
+ # Check if it has the expected structure
240
+ plans_file = os.path.join(task_dir, "nnUNetTrainerV2__nnUNetPlansv2.1")
241
+ if os.path.exists(plans_file):
242
+ logger.info(f"Model already exists for task '{task_id}' with {model_type}")
243
+ return True
244
+
245
+ # Download URL for the task
246
+ download_url = f"https://zenodo.org/record/4003545/files/{task_id}.zip?download=1"
247
+
248
+ # Create temporary file for download
249
+ temp_zip = os.path.join(nnunet_dir, f"{task_id}_temp.zip")
250
+
251
+ # Download with browser headers
252
+ if self._download_model_with_browser_headers(download_url, temp_zip):
253
+ try:
254
+ logger.info(f"Extracting {temp_zip} to {nnunet_dir}")
255
+ with zipfile.ZipFile(temp_zip, "r") as zip_ref:
256
+ zip_ref.extractall(nnunet_dir)
257
+
258
+ # Remove temporary zip file
259
+ os.remove(temp_zip)
260
+
261
+ # Verify extraction worked
262
+ if os.path.exists(task_dir):
263
+ logger.info(f"Model successfully downloaded and extracted for task '{task_id}'")
264
+ logger.info(f"Model directory: {task_dir}")
265
+ return True
266
+ else:
267
+ logger.error(f"Task directory not found after extraction: {task_dir}")
268
+ return False
269
+
270
+ except Exception as e:
271
+ logger.error(f"Extraction failed: {e}")
272
+ if os.path.exists(temp_zip):
273
+ os.remove(temp_zip)
274
+ return False
275
+ else:
276
+ return False
277
+
278
+ def segment_with_nn_unet(
279
+ self,
280
+ image_path,
281
+ output_dir,
282
+ task_id,
283
+ model_type="3d_fullres",
284
+ folds=None,
285
+ use_tta=False,
286
+ num_threads=1,
287
+ mixed_precision=True,
288
+ verbose=True,
289
+ auto_prepare_input=True,
290
+ results_folder=None,
291
+ auto_download=True,
292
+ ):
293
+ """
294
+ Segment images using nnUNet with proper environment setup and automatic model downloading
295
+ Args:
296
+ image_path: Path to input image file or directory
297
+ output_dir: Directory to save segmentation results
298
+ task_id: Task identifier (e.g., 'Task001_BrainTumour')
299
+ model_type: Model type (default: '3d_fullres')
300
+ folds: Model folds to use (default: [0, 1, 2, 3, 4])
301
+ use_tta: Use test time augmentation (default: False)
302
+ num_threads: Number of threads for preprocessing (default: 1)
303
+ mixed_precision: Use mixed precision (default: True)
304
+ verbose: Verbose logging (default: True)
305
+ auto_prepare_input: Automatically prepare input for nnUNet (default: True)
306
+ results_folder: Path to nnUNet results folder (default: None, will use environment variable or default)
307
+ auto_download: Automatically download missing models (default: True)
308
+ """
309
+ if folds is None:
310
+ folds = [0, 1, 2, 3, 4]
311
+ os.makedirs(output_dir, exist_ok=True)
312
+ logging.basicConfig(level=logging.INFO if verbose else logging.WARNING)
313
+
314
+ # Setup nnUNet environment first
315
+ self.setup_nnunet_environment(results_folder=results_folder)
316
+
317
+ # Prepare input data if requested
318
+ if auto_prepare_input:
319
+ temp_input_dir = os.path.join(output_dir, "temp_input")
320
+ prepared_input_dir = self.prepare_input_for_nnunet(image_path, temp_input_dir)
321
+ image_path = prepared_input_dir
322
+ logging.info(f"Input prepared for nnUNet: {image_path}")
323
+
324
+ logging.info("Verifying NIfTI input files...")
325
+
326
+ def verify_nifti_input(image_path):
327
+ if os.path.isfile(image_path):
328
+ nib.load(image_path)
329
+ else:
330
+ for file in os.listdir(image_path):
331
+ if file.endswith(".nii") or file.endswith(".nii.gz"):
332
+ nib.load(os.path.join(image_path, file))
333
+
334
+ verify_nifti_input(image_path)
335
+
336
+ # Get or set up the results folder
337
+ results_folder = os.environ.get("nnUNet_RESULTS_FOLDER")
338
+ if not results_folder:
339
+ # Try common locations
340
+ common_paths = [
341
+ "./models/nnUNet",
342
+ "~/nnUNet_results",
343
+ "~/biomni_models/nnUNet",
344
+ ]
345
+
346
+ for base_path in common_paths:
347
+ expanded_path = os.path.expanduser(base_path)
348
+ if os.path.exists(expanded_path):
349
+ results_folder = expanded_path
350
+ break
351
+
352
+ # If still no folder found, create one
353
+ if not results_folder:
354
+ results_folder = os.path.expanduser("~/nnUNet_results")
355
+ os.makedirs(results_folder, exist_ok=True)
356
+ logging.info(f"Created new results folder: {results_folder}")
357
+
358
+ os.environ["RESULTS_FOLDER"] = results_folder
359
+ os.environ["nnUNet_RESULTS_FOLDER"] = results_folder
360
+ logging.info(f"Set RESULTS_FOLDER environment variable to: {results_folder}")
361
+
362
+ # Construct the expected model path - using the correct structure
363
+ model_folder = os.path.join(results_folder, "nnUNet", model_type, task_id, "nnUNetTrainerV2__nnUNetPlansv2.1")
364
+
365
+ logging.info(f"Looking for model at: {model_folder}")
366
+
367
+ # Check if model files actually exist (not just the directory)
368
+ def check_model_files_exist(model_folder, folds):
369
+ """Check if actual model weight files exist for the specified folds"""
370
+ if not os.path.exists(model_folder):
371
+ return False
372
+
373
+ # Check for plans.pkl and other required files
374
+ required_files = ["plans.pkl"]
375
+ for req_file in required_files:
376
+ if not os.path.exists(os.path.join(model_folder, req_file)):
377
+ return False
378
+
379
+ # Check if at least one fold has model files
380
+ for fold in folds:
381
+ fold_dir = os.path.join(model_folder, f"fold_{fold}")
382
+ if os.path.exists(fold_dir):
383
+ # Look for model files (.model, .model.pkl, etc.)
384
+ model_files = [f for f in os.listdir(fold_dir) if f.endswith((".model", ".model.pkl"))]
385
+ if model_files:
386
+ return True
387
+ return False
388
+
389
+ # Check if model exists, download if not
390
+ if not check_model_files_exist(model_folder, folds):
391
+ if auto_download:
392
+ logging.info(f"Model weights for {task_id} not found. Downloading...")
393
+ # Ensure the results folder structure exists
394
+ os.makedirs(os.path.dirname(model_folder), exist_ok=True)
395
+
396
+ # Download the model using our custom download function
397
+ if self._download_and_extract_model(task_id, model_type):
398
+ logging.info(f"Downloaded pretrained model for {task_id} successfully.")
399
+ else:
400
+ raise RuntimeError(f"Failed to download model for {task_id}")
401
+
402
+ # Verify the download worked
403
+ if not check_model_files_exist(model_folder, folds):
404
+ raise RuntimeError(
405
+ f"Model download completed but files not found at expected location: {model_folder}"
406
+ )
407
+ else:
408
+ # Ask user for permission to download
409
+ user_input = (
410
+ input(f"Model weights for {task_id} not found. Do you want to download them? (y/n): ")
411
+ .strip()
412
+ .lower()
413
+ )
414
+ if user_input == "y":
415
+ # Download the model using our custom download function
416
+ if self._download_and_extract_model(task_id, model_type):
417
+ logging.info(f"Downloaded pretrained model for {task_id} successfully.")
418
+ else:
419
+ raise RuntimeError(f"Failed to download model for {task_id}")
420
+ else:
421
+ raise RuntimeError("Model weights not found and download declined by user.")
422
+
423
+ # Double-check that we now have the model
424
+ if not check_model_files_exist(model_folder, folds):
425
+ raise RuntimeError(f"Model files still not found at {model_folder} after download attempt")
426
+
427
+ logging.info(f"Using model: {model_folder}")
428
+
429
+ # Patch torch.load for compatibility
430
+ original_torch_load = torch.load
431
+
432
+ def patched_torch_load(*args, **kwargs):
433
+ kwargs["weights_only"] = False
434
+ return original_torch_load(*args, **kwargs)
435
+
436
+ # Run the segmentation
437
+ torch.load = patched_torch_load
438
+ try:
439
+ predict_from_folder(
440
+ model=model_folder,
441
+ input_folder=image_path,
442
+ output_folder=output_dir,
443
+ folds=folds,
444
+ save_npz=False,
445
+ num_threads_preprocessing=num_threads,
446
+ num_threads_nifti_save=num_threads,
447
+ mixed_precision=mixed_precision,
448
+ lowres_segmentations=None,
449
+ part_id=0,
450
+ num_parts=1,
451
+ tta=use_tta,
452
+ )
453
+ finally:
454
+ # Restore original torch.load behavior
455
+ torch.load = original_torch_load
456
+
457
+ # Clean up temporary input directory if it was created
458
+ if auto_prepare_input and os.path.exists(temp_input_dir):
459
+ import shutil
460
+
461
+ shutil.rmtree(temp_input_dir)
462
+ logging.info("Cleaned up temporary input directory")
463
+
464
+ logging.info(f"Segmentation outputs stored at: {output_dir}")
465
+ return output_dir
466
+
467
+ def create_segmentation_visualization(self, original_mri, segmentation, output_dir="./visualization_output"):
468
+ """
469
+ Create and save visualization of segmentation results using nilearn
470
+ Args:
471
+ original_mri: Path to original MRI file
472
+ segmentation: Path to segmentation file
473
+ output_dir: Directory to save visualization images
474
+ Returns:
475
+ list: List of saved image file paths
476
+ """
477
+ try:
478
+ # Import nilearn here to avoid dependency issues
479
+ from nilearn import plotting
480
+
481
+ # Create output directory
482
+ os.makedirs(output_dir, exist_ok=True)
483
+
484
+ # Check if files exist
485
+ if not os.path.exists(original_mri):
486
+ raise FileNotFoundError(f"Original MRI file not found: {original_mri}")
487
+
488
+ if not os.path.exists(segmentation):
489
+ raise FileNotFoundError(f"Segmentation file not found: {segmentation}")
490
+
491
+ print("✅ Files found, creating visualizations...")
492
+ saved_files = []
493
+
494
+ # Create and save the main overlay plot
495
+ display = plotting.plot_roi(
496
+ segmentation,
497
+ bg_img=original_mri,
498
+ cmap="Set1",
499
+ alpha=0.6,
500
+ title="Segmentation Overlay",
501
+ )
502
+
503
+ # Save the main overlay plot
504
+ output_file = os.path.join(output_dir, "segmentation_overlay.png")
505
+ display.savefig(output_file, dpi=150, bbox_inches="tight")
506
+ saved_files.append(output_file)
507
+ print(f"✅ Main overlay saved to: {output_file}")
508
+ display.close()
509
+
510
+ # Create additional views and save them
511
+ # Axial view
512
+ display_axial = plotting.plot_roi(
513
+ segmentation,
514
+ bg_img=original_mri,
515
+ cmap="Set1",
516
+ alpha=0.6,
517
+ title="Segmentation Overlay - Axial View",
518
+ display_mode="z",
519
+ )
520
+ axial_file = os.path.join(output_dir, "segmentation_axial.png")
521
+ display_axial.savefig(axial_file, dpi=150, bbox_inches="tight")
522
+ saved_files.append(axial_file)
523
+ print(f"✅ Axial view saved to: {axial_file}")
524
+ display_axial.close()
525
+
526
+ # Sagittal view
527
+ display_sagittal = plotting.plot_roi(
528
+ segmentation,
529
+ bg_img=original_mri,
530
+ cmap="Set1",
531
+ alpha=0.6,
532
+ title="Segmentation Overlay - Sagittal View",
533
+ display_mode="x",
534
+ )
535
+ sagittal_file = os.path.join(output_dir, "segmentation_sagittal.png")
536
+ display_sagittal.savefig(sagittal_file, dpi=150, bbox_inches="tight")
537
+ saved_files.append(sagittal_file)
538
+ print(f"✅ Sagittal view saved to: {sagittal_file}")
539
+ display_sagittal.close()
540
+
541
+ # Coronal view
542
+ display_coronal = plotting.plot_roi(
543
+ segmentation,
544
+ bg_img=original_mri,
545
+ cmap="Set1",
546
+ alpha=0.6,
547
+ title="Segmentation Overlay - Coronal View",
548
+ display_mode="y",
549
+ )
550
+ coronal_file = os.path.join(output_dir, "segmentation_coronal.png")
551
+ display_coronal.savefig(coronal_file, dpi=150, bbox_inches="tight")
552
+ saved_files.append(coronal_file)
553
+ print(f"✅ Coronal view saved to: {coronal_file}")
554
+ display_coronal.close()
555
+
556
+ print(f"\n✅ All visualizations saved to: {output_dir}")
557
+ print("Files created:")
558
+ for file_path in saved_files:
559
+ print(f" - {os.path.basename(file_path)}")
560
+
561
+ return saved_files
562
+
563
+ except ImportError:
564
+ print(" nilearn not available. Install with: pip install nilearn")
565
+ return []
566
+ except Exception as e:
567
+ print(f" Visualization failed: {e}")
568
+ import traceback
569
+
570
+ traceback.print_exc()
571
+ return []
572
+
573
+
574
+ # ============================================================================
575
+ # IMAGE REGISTRATION CLASS
576
+ # ============================================================================
577
+
578
+
579
+ class ImageRegistrationTool:
580
+ """
581
+ A comprehensive tool for medical image registration using SimpleITK.
582
+ Supports rigid, affine, and deformable registration with preprocessing and visualization.
583
+ """
584
+
585
+ def __init__(self):
586
+ """Initialize the ImageRegistrationTool."""
587
+ self.supported_formats = [".nii", ".nii.gz", ".nrrd", ".mha", ".mhd"]
588
+ logger.info("ImageRegistrationTool initialized")
589
+
590
+ def load_image(self, image_path: str) -> sitk.Image:
591
+ """
592
+ Load a medical image using SimpleITK.
593
+
594
+ Args:
595
+ image_path: Path to the image file
596
+
597
+ Returns:
598
+ SimpleITK Image object
599
+ """
600
+ if not os.path.exists(image_path):
601
+ raise FileNotFoundError(f"Image file not found: {image_path}")
602
+
603
+ logger.info(f"Loading image: {image_path}")
604
+ try:
605
+ image = sitk.ReadImage(image_path)
606
+ logger.info(f"Successfully loaded image with size: {image.GetSize()}")
607
+ return image
608
+ except Exception as e:
609
+ logger.error(f"Failed to load image {image_path}: {e}")
610
+ raise
611
+
612
+ def save_image(self, image: sitk.Image, output_path: str) -> None:
613
+ """
614
+ Save a SimpleITK image to file.
615
+
616
+ Args:
617
+ image: SimpleITK Image object
618
+ output_path: Path to save the image (must include filename and extension)
619
+ """
620
+ # Validate output path
621
+ if os.path.isdir(output_path):
622
+ raise ValueError(
623
+ f"Output path '{output_path}' is a directory. Please provide a file path with extension (e.g., '.nii.gz', '.png', '.jpg')"
624
+ )
625
+
626
+ if not os.path.splitext(output_path)[1]:
627
+ raise ValueError(
628
+ f"Output path '{output_path}' has no file extension. Please add an extension (e.g., '.nii.gz', '.png', '.jpg')"
629
+ )
630
+
631
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
632
+ logger.info(f"Saving image to: {output_path}")
633
+ try:
634
+ sitk.WriteImage(image, output_path)
635
+ logger.info("Image saved successfully")
636
+ except Exception as e:
637
+ logger.error(f"Failed to save image to {output_path}: {e}")
638
+ raise
639
+
640
+ def preprocess_image(self, image: sitk.Image, denoise: bool = True, normalize: bool = True) -> sitk.Image:
641
+ """
642
+ Preprocess an image with denoising and normalization.
643
+
644
+ Args:
645
+ image: Input SimpleITK image
646
+ denoise: Whether to apply denoising
647
+ normalize: Whether to apply normalization
648
+
649
+ Returns:
650
+ Preprocessed SimpleITK image
651
+ """
652
+ logger.info("Preprocessing image...")
653
+ processed_image = sitk.Image(image)
654
+
655
+ if denoise:
656
+ # Apply Gaussian smoothing for denoising
657
+ processed_image = sitk.SmoothingRecursiveGaussian(processed_image, sigma=1.0)
658
+ logger.info("Applied denoising")
659
+
660
+ if normalize:
661
+ # Normalize to [0, 1] range
662
+ min_max_filter = sitk.MinimumMaximumImageFilter()
663
+ min_max_filter.Execute(processed_image)
664
+ min_val = min_max_filter.GetMinimum()
665
+ max_val = min_max_filter.GetMaximum()
666
+
667
+ if max_val > min_val:
668
+ processed_image = sitk.IntensityWindowing(
669
+ processed_image, windowMinimum=min_val, windowMaximum=max_val, outputMinimum=0.0, outputMaximum=1.0
670
+ )
671
+ logger.info("Applied normalization")
672
+
673
+ return processed_image
674
+
675
+ def create_rigid_transform(
676
+ self, fixed_image: sitk.Image, moving_image: sitk.Image, initial_transform: sitk.Transform | None = None
677
+ ) -> sitk.Transform:
678
+ """
679
+ Create a rigid transform for image registration.
680
+
681
+ Args:
682
+ fixed_image: Reference (fixed) image
683
+ moving_image: Image to be registered
684
+ initial_transform: Optional initial transform
685
+
686
+ Returns:
687
+ Rigid transform object
688
+ """
689
+ logger.info("Creating rigid transform...")
690
+
691
+ if initial_transform is None:
692
+ # Create identity transform
693
+ transform = sitk.Euler3DTransform()
694
+ else:
695
+ transform = initial_transform
696
+
697
+ return transform
698
+
699
+ def create_affine_transform(
700
+ self, fixed_image: sitk.Image, moving_image: sitk.Image, initial_transform: sitk.Transform | None = None
701
+ ) -> sitk.Transform:
702
+ """
703
+ Create an affine transform for image registration.
704
+
705
+ Args:
706
+ fixed_image: Reference (fixed) image
707
+ moving_image: Image to be registered
708
+ initial_transform: Optional initial transform
709
+
710
+ Returns:
711
+ Affine transform object
712
+ """
713
+ logger.info("Creating affine transform...")
714
+
715
+ if initial_transform is None:
716
+ # Create identity transform
717
+ transform = sitk.AffineTransform(3)
718
+ else:
719
+ transform = initial_transform
720
+
721
+ return transform
722
+
723
+ def create_deformable_transform(
724
+ self, fixed_image: sitk.Image, moving_image: sitk.Image, number_of_control_points: int = 4
725
+ ) -> sitk.Transform:
726
+ """
727
+ Create a deformable (B-spline) transform for image registration.
728
+
729
+ Args:
730
+ fixed_image: Reference (fixed) image
731
+ moving_image: Image to be registered
732
+ number_of_control_points: Number of B-spline control points per dimension
733
+
734
+ Returns:
735
+ Deformable transform object
736
+ """
737
+ logger.info("Creating deformable transform...")
738
+
739
+ # Create B-spline transform
740
+ transform = sitk.BSplineTransformInitializer(fixed_image, [number_of_control_points] * 3, order=3)
741
+
742
+ return transform
743
+
744
+ def setup_registration_method(
745
+ self,
746
+ transform: sitk.Transform,
747
+ metric: str = "mutual_information",
748
+ optimizer: str = "gradient_descent",
749
+ learning_rate: float = 0.01,
750
+ number_of_iterations: int = 100,
751
+ gradient_convergence_tolerance: float = 1e-6,
752
+ ) -> sitk.ImageRegistrationMethod:
753
+ """
754
+ Setup the registration method with specified parameters.
755
+
756
+ Args:
757
+ transform: Transform object
758
+ metric: Similarity metric name
759
+ optimizer: Optimizer name
760
+ learning_rate: Learning rate for gradient descent
761
+ number_of_iterations: Maximum number of iterations
762
+ gradient_convergence_tolerance: Convergence tolerance
763
+
764
+ Returns:
765
+ Configured registration method
766
+ """
767
+ logger.info(f"Setting up registration method: {metric} metric, {optimizer} optimizer")
768
+
769
+ registration_method = sitk.ImageRegistrationMethod()
770
+
771
+ # Set similarity metric
772
+ if metric == "mutual_information":
773
+ registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
774
+ elif metric == "mean_squares":
775
+ registration_method.SetMetricAsMeanSquares()
776
+ elif metric == "correlation":
777
+ registration_method.SetMetricAsCorrelation()
778
+ elif metric == "normalized_correlation":
779
+ registration_method.SetMetricAsNormalizedCorrelation()
780
+ else:
781
+ logger.warning(f"Unknown metric {metric}, using mutual information")
782
+ registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
783
+
784
+ # Set optimizer
785
+ if optimizer == "gradient_descent":
786
+ registration_method.SetOptimizerAsGradientDescent(
787
+ learningRate=learning_rate,
788
+ numberOfIterations=number_of_iterations,
789
+ convergenceMinimumValue=gradient_convergence_tolerance,
790
+ )
791
+ elif optimizer == "lbfgsb":
792
+ registration_method.SetOptimizerAsLBFGSB(
793
+ gradientConvergenceTolerance=gradient_convergence_tolerance, numberOfIterations=number_of_iterations
794
+ )
795
+ elif optimizer == "powell":
796
+ registration_method.SetOptimizerAsPowell(numberOfIterations=number_of_iterations, maximumLineIterations=20)
797
+ elif optimizer == "amoeba":
798
+ registration_method.SetOptimizerAsAmoeba(
799
+ numberOfIterations=number_of_iterations, parametersConvergenceTolerance=gradient_convergence_tolerance
800
+ )
801
+ else:
802
+ logger.warning(f"Unknown optimizer {optimizer}, using gradient descent")
803
+ registration_method.SetOptimizerAsGradientDescent(
804
+ learningRate=learning_rate,
805
+ numberOfIterations=number_of_iterations,
806
+ convergenceMinimumValue=gradient_convergence_tolerance,
807
+ )
808
+
809
+ # Set interpolator
810
+ registration_method.SetInterpolator(sitk.sitkLinear)
811
+
812
+ # Set initial transform
813
+ registration_method.SetInitialTransform(transform, inPlace=False)
814
+
815
+ return registration_method
816
+
817
+ def register_images(
818
+ self,
819
+ fixed_image: sitk.Image,
820
+ moving_image: sitk.Image,
821
+ transform: sitk.Transform,
822
+ registration_method: sitk.ImageRegistrationMethod,
823
+ ) -> tuple[sitk.Transform, sitk.Image]:
824
+ """
825
+ Perform image registration.
826
+
827
+ Args:
828
+ fixed_image: Reference (fixed) image
829
+ moving_image: Image to be registered
830
+ transform: Transform object
831
+ registration_method: Configured registration method
832
+
833
+ Returns:
834
+ Tuple of (final_transform, registered_image)
835
+ """
836
+ logger.info("Starting image registration...")
837
+
838
+ # Add iteration callback
839
+ def command_iteration():
840
+ logger.info(
841
+ f"Iteration: {registration_method.GetOptimizerIteration()}, "
842
+ f"Metric value: {registration_method.GetMetricValue():.6f}"
843
+ )
844
+
845
+ registration_method.AddCommand(sitk.sitkIterationEvent, command_iteration)
846
+
847
+ try:
848
+ # Execute registration
849
+ final_transform = registration_method.Execute(fixed_image, moving_image)
850
+
851
+ # Apply transform to moving image
852
+ resampler = sitk.ResampleImageFilter()
853
+ resampler.SetReferenceImage(fixed_image)
854
+ resampler.SetInterpolator(sitk.sitkLinear)
855
+ resampler.SetDefaultPixelValue(0)
856
+ resampler.SetTransform(final_transform)
857
+
858
+ registered_image = resampler.Execute(moving_image)
859
+
860
+ logger.info("Registration completed successfully")
861
+ return final_transform, registered_image
862
+
863
+ except Exception as e:
864
+ logger.error(f"Registration failed: {e}")
865
+ raise
866
+
867
+ def calculate_similarity_metrics(self, image1: sitk.Image, image2: sitk.Image) -> dict[str, float]:
868
+ """
869
+ Calculate similarity metrics between two images.
870
+
871
+ Args:
872
+ image1: First image
873
+ image2: Second image
874
+
875
+ Returns:
876
+ Dictionary of similarity metrics
877
+ """
878
+ logger.info("Calculating similarity metrics...")
879
+ metrics = {}
880
+
881
+ try:
882
+ # Convert to numpy arrays
883
+ array1 = sitk.GetArrayFromImage(image1)
884
+ array2 = sitk.GetArrayFromImage(image2)
885
+
886
+ # Flatten arrays
887
+ flat1 = array1.flatten()
888
+ flat2 = array2.flatten()
889
+
890
+ # Remove invalid values
891
+ valid_mask = np.isfinite(flat1) & np.isfinite(flat2)
892
+ flat1 = flat1[valid_mask]
893
+ flat2 = flat2[valid_mask]
894
+
895
+ if len(flat1) == 0:
896
+ logger.warning("No valid pixels for similarity calculation")
897
+ return {
898
+ "mutual_information": 0.0,
899
+ "mean_squares": 0.0,
900
+ "correlation": 0.0,
901
+ "normalized_correlation": 0.0,
902
+ }
903
+
904
+ # Mean Squared Error
905
+ mse = np.mean((flat1 - flat2) ** 2)
906
+ metrics["mean_squares"] = -mse # Negative because we want to maximize
907
+
908
+ # Pearson Correlation
909
+ if np.std(flat1) > 0 and np.std(flat2) > 0:
910
+ correlation = np.corrcoef(flat1, flat2)[0, 1]
911
+ metrics["correlation"] = correlation if not np.isnan(correlation) else 0.0
912
+ else:
913
+ metrics["correlation"] = 0.0
914
+
915
+ # Normalized Cross Correlation
916
+ if np.std(flat1) > 0 and np.std(flat2) > 0:
917
+ ncc = np.corrcoef(flat1, flat2)[0, 1]
918
+ metrics["normalized_correlation"] = ncc if not np.isnan(ncc) else 0.0
919
+ else:
920
+ metrics["normalized_correlation"] = 0.0
921
+
922
+ # Mutual Information (simplified calculation)
923
+ try:
924
+ # Create 2D histogram
925
+ hist_2d, x_edges, y_edges = np.histogram2d(flat1, flat2, bins=50)
926
+ hist_2d = hist_2d + 1e-10 # Add small value to avoid log(0)
927
+
928
+ # Normalize histogram
929
+ pxy = hist_2d / np.sum(hist_2d)
930
+ px = np.sum(pxy, axis=1)
931
+ py = np.sum(pxy, axis=0)
932
+
933
+ # Calculate mutual information
934
+ mi = 0.0
935
+ for i in range(len(px)):
936
+ for j in range(len(py)):
937
+ if pxy[i, j] > 0 and px[i] > 0 and py[j] > 0:
938
+ mi += pxy[i, j] * np.log2(pxy[i, j] / (px[i] * py[j]))
939
+
940
+ metrics["mutual_information"] = mi
941
+
942
+ except Exception as e:
943
+ logger.warning(f"Failed to calculate mutual information: {e}")
944
+ metrics["mutual_information"] = 0.0
945
+
946
+ except Exception as e:
947
+ logger.warning(f"Failed to calculate similarity metrics: {e}")
948
+ metrics = {
949
+ "mutual_information": 0.0,
950
+ "mean_squares": 0.0,
951
+ "correlation": 0.0,
952
+ "normalized_correlation": 0.0,
953
+ }
954
+
955
+ logger.info("Similarity metrics calculated")
956
+ return metrics
957
+
958
+
959
+ # ============================================================================
960
+ # CONVENIENCE FUNCTIONS FOR BIOMNI INTEGRATION
961
+ # ============================================================================
962
+
963
+
964
+ # Segmentation convenience functions
965
+ def split_modalities(input_file, output_dir, case_name="BRAT"):
966
+ """Convenience function for splitting modalities"""
967
+ tool = SegmentationTool()
968
+ return tool.split_modalities(input_file, output_dir, case_name)
969
+
970
+
971
+ def prepare_input_for_nnunet(input_path, output_dir, case_name="BRAT"):
972
+ """Convenience function for preparing nnUNet input"""
973
+ tool = SegmentationTool()
974
+ return tool.prepare_input_for_nnunet(input_path, output_dir, case_name)
975
+
976
+
977
+ def segment_with_nn_unet(
978
+ image_path,
979
+ output_dir,
980
+ task_id,
981
+ model_type="3d_fullres",
982
+ folds=None,
983
+ use_tta=False,
984
+ num_threads=1,
985
+ mixed_precision=True,
986
+ verbose=True,
987
+ auto_prepare_input=True,
988
+ results_folder=None,
989
+ ):
990
+ """Convenience function for nnUNet segmentation"""
991
+ tool = SegmentationTool()
992
+ return tool.segment_with_nn_unet(
993
+ image_path,
994
+ output_dir,
995
+ task_id,
996
+ model_type,
997
+ folds,
998
+ use_tta,
999
+ num_threads,
1000
+ mixed_precision,
1001
+ verbose,
1002
+ auto_prepare_input,
1003
+ results_folder,
1004
+ )
1005
+
1006
+
1007
+ def create_segmentation_visualization(original_mri, segmentation, output_dir="./visualization_output"):
1008
+ """Convenience function for segmentation visualization"""
1009
+ tool = SegmentationTool()
1010
+ return tool.create_segmentation_visualization(original_mri, segmentation, output_dir)
1011
+
1012
+
1013
+ # Registration convenience functions
1014
+ def preprocess_image(image_path: str, output_path: str, denoise: bool = True, normalize: bool = True) -> str:
1015
+ """
1016
+ Standalone image preprocessing function for Biomni integration.
1017
+
1018
+ Args:
1019
+ image_path: Path to input image
1020
+ output_path: Path to save preprocessed image
1021
+ denoise: Whether to apply denoising (default: True)
1022
+ normalize: Whether to apply normalization (default: True)
1023
+
1024
+ Returns:
1025
+ Path to the saved preprocessed image
1026
+ """
1027
+ tool = ImageRegistrationTool()
1028
+ image = tool.load_image(image_path)
1029
+ preprocessed_image = tool.preprocess_image(image, denoise, normalize)
1030
+ tool.save_image(preprocessed_image, output_path)
1031
+ return output_path
1032
+
1033
+
1034
+ def quick_rigid_registration(
1035
+ fixed_image_path: str,
1036
+ moving_image_path: str,
1037
+ output_dir: str,
1038
+ metric: str = "mutual_information",
1039
+ optimizer: str = "gradient_descent",
1040
+ preprocess: bool = True,
1041
+ create_visualizations: bool = True,
1042
+ learning_rate: float = 0.01,
1043
+ number_of_iterations: int = 100,
1044
+ gradient_convergence_tolerance: float = 1e-6,
1045
+ ) -> dict:
1046
+ """
1047
+ Quick rigid registration function for Biomni integration.
1048
+
1049
+ Args:
1050
+ fixed_image_path: Path to reference image
1051
+ moving_image_path: Path to image to register
1052
+ output_dir: Directory to save results
1053
+ metric: Similarity metric
1054
+ optimizer: Optimization method
1055
+ preprocess: Whether to preprocess images
1056
+ create_visualizations: Whether to create visualizations
1057
+ learning_rate: Learning rate for optimizer
1058
+ number_of_iterations: Maximum iterations
1059
+ gradient_convergence_tolerance: Convergence tolerance
1060
+
1061
+ Returns:
1062
+ Dictionary with registration results
1063
+ """
1064
+ tool = ImageRegistrationTool()
1065
+
1066
+ # Load images
1067
+ fixed_image = tool.load_image(fixed_image_path)
1068
+ moving_image = tool.load_image(moving_image_path)
1069
+
1070
+ # Preprocess if requested
1071
+ if preprocess:
1072
+ fixed_image = tool.preprocess_image(fixed_image)
1073
+ moving_image = tool.preprocess_image(moving_image)
1074
+
1075
+ # Create transform and registration method
1076
+ transform = tool.create_rigid_transform(fixed_image, moving_image)
1077
+ registration_method = tool.setup_registration_method(
1078
+ transform, metric, optimizer, learning_rate, number_of_iterations, gradient_convergence_tolerance
1079
+ )
1080
+
1081
+ # Perform registration
1082
+ final_transform, registered_image = tool.register_images(fixed_image, moving_image, transform, registration_method)
1083
+
1084
+ # Save results
1085
+ os.makedirs(output_dir, exist_ok=True)
1086
+
1087
+ # Save registered image
1088
+ registered_path = os.path.join(output_dir, "rigid_registered.nii.gz")
1089
+ tool.save_image(registered_image, registered_path)
1090
+
1091
+ # Save transform
1092
+ transform_path = os.path.join(output_dir, "rigid_transform.tfm")
1093
+ sitk.WriteTransform(final_transform, transform_path)
1094
+
1095
+ # Calculate metrics
1096
+ metrics_before = tool.calculate_similarity_metrics(fixed_image, moving_image)
1097
+ metrics_after = tool.calculate_similarity_metrics(fixed_image, registered_image)
1098
+
1099
+ results = {
1100
+ "registered_image_path": registered_path,
1101
+ "transform_path": transform_path,
1102
+ "metrics_before": metrics_before,
1103
+ "metrics_after": metrics_after,
1104
+ "registration_type": "rigid",
1105
+ }
1106
+
1107
+ return results
1108
+
1109
+
1110
+ def quick_affine_registration(
1111
+ fixed_image_path: str,
1112
+ moving_image_path: str,
1113
+ output_dir: str,
1114
+ metric: str = "mutual_information",
1115
+ optimizer: str = "gradient_descent",
1116
+ preprocess: bool = True,
1117
+ create_visualizations: bool = True,
1118
+ learning_rate: float = 0.01,
1119
+ number_of_iterations: int = 100,
1120
+ gradient_convergence_tolerance: float = 1e-6,
1121
+ ) -> dict:
1122
+ """
1123
+ Quick affine registration function for Biomni integration.
1124
+
1125
+ Args:
1126
+ fixed_image_path: Path to reference image
1127
+ moving_image_path: Path to image to register
1128
+ output_dir: Directory to save results
1129
+ metric: Similarity metric
1130
+ optimizer: Optimization method
1131
+ preprocess: Whether to preprocess images
1132
+ create_visualizations: Whether to create visualizations
1133
+ learning_rate: Learning rate for optimizer
1134
+ number_of_iterations: Maximum iterations
1135
+ gradient_convergence_tolerance: Convergence tolerance
1136
+
1137
+ Returns:
1138
+ Dictionary with registration results
1139
+ """
1140
+ tool = ImageRegistrationTool()
1141
+
1142
+ # Load images
1143
+ fixed_image = tool.load_image(fixed_image_path)
1144
+ moving_image = tool.load_image(moving_image_path)
1145
+
1146
+ # Preprocess if requested
1147
+ if preprocess:
1148
+ fixed_image = tool.preprocess_image(fixed_image)
1149
+ moving_image = tool.preprocess_image(moving_image)
1150
+
1151
+ # Create transform and registration method
1152
+ transform = tool.create_affine_transform(fixed_image, moving_image)
1153
+ registration_method = tool.setup_registration_method(
1154
+ transform, metric, optimizer, learning_rate, number_of_iterations, gradient_convergence_tolerance
1155
+ )
1156
+
1157
+ # Perform registration
1158
+ final_transform, registered_image = tool.register_images(fixed_image, moving_image, transform, registration_method)
1159
+
1160
+ # Save results
1161
+ os.makedirs(output_dir, exist_ok=True)
1162
+
1163
+ # Save registered image
1164
+ registered_path = os.path.join(output_dir, "affine_registered.nii.gz")
1165
+ tool.save_image(registered_image, registered_path)
1166
+
1167
+ # Save transform
1168
+ transform_path = os.path.join(output_dir, "affine_transform.tfm")
1169
+ sitk.WriteTransform(final_transform, transform_path)
1170
+
1171
+ # Calculate metrics
1172
+ metrics_before = tool.calculate_similarity_metrics(fixed_image, moving_image)
1173
+ metrics_after = tool.calculate_similarity_metrics(fixed_image, registered_image)
1174
+
1175
+ results = {
1176
+ "registered_image_path": registered_path,
1177
+ "transform_path": transform_path,
1178
+ "metrics_before": metrics_before,
1179
+ "metrics_after": metrics_after,
1180
+ "registration_type": "affine",
1181
+ }
1182
+
1183
+ return results
1184
+
1185
+
1186
+ def quick_deformable_registration(
1187
+ fixed_image_path: str,
1188
+ moving_image_path: str,
1189
+ output_dir: str,
1190
+ metric: str = "mutual_information",
1191
+ optimizer: str = "gradient_descent",
1192
+ preprocess: bool = True,
1193
+ create_visualizations: bool = True,
1194
+ learning_rate: float = 0.01,
1195
+ number_of_iterations: int = 100,
1196
+ gradient_convergence_tolerance: float = 1e-6,
1197
+ number_of_control_points: int = 4,
1198
+ ) -> dict:
1199
+ """
1200
+ Quick deformable registration function for Biomni integration.
1201
+
1202
+ Args:
1203
+ fixed_image_path: Path to reference image
1204
+ moving_image_path: Path to image to register
1205
+ output_dir: Directory to save results
1206
+ metric: Similarity metric
1207
+ optimizer: Optimization method
1208
+ preprocess: Whether to preprocess images
1209
+ create_visualizations: Whether to create visualizations
1210
+ learning_rate: Learning rate for optimizer
1211
+ number_of_iterations: Maximum iterations
1212
+ gradient_convergence_tolerance: Convergence tolerance
1213
+ number_of_control_points: Number of B-spline control points
1214
+
1215
+ Returns:
1216
+ Dictionary with registration results
1217
+ """
1218
+ tool = ImageRegistrationTool()
1219
+
1220
+ # Load images
1221
+ fixed_image = tool.load_image(fixed_image_path)
1222
+ moving_image = tool.load_image(moving_image_path)
1223
+
1224
+ # Preprocess if requested
1225
+ if preprocess:
1226
+ fixed_image = tool.preprocess_image(fixed_image)
1227
+ moving_image = tool.preprocess_image(moving_image)
1228
+
1229
+ # Create transform and registration method
1230
+ transform = tool.create_deformable_transform(fixed_image, moving_image, number_of_control_points)
1231
+ registration_method = tool.setup_registration_method(
1232
+ transform, metric, optimizer, learning_rate, number_of_iterations, gradient_convergence_tolerance
1233
+ )
1234
+
1235
+ # Perform registration
1236
+ final_transform, registered_image = tool.register_images(fixed_image, moving_image, transform, registration_method)
1237
+
1238
+ # Save results
1239
+ os.makedirs(output_dir, exist_ok=True)
1240
+
1241
+ # Save registered image
1242
+ registered_path = os.path.join(output_dir, "deformable_registered.nii.gz")
1243
+ tool.save_image(registered_image, registered_path)
1244
+
1245
+ # Save transform
1246
+ transform_path = os.path.join(output_dir, "deformable_transform.tfm")
1247
+ sitk.WriteTransform(final_transform, transform_path)
1248
+
1249
+ # Calculate metrics
1250
+ metrics_before = tool.calculate_similarity_metrics(fixed_image, moving_image)
1251
+ metrics_after = tool.calculate_similarity_metrics(fixed_image, registered_image)
1252
+
1253
+ results = {
1254
+ "registered_image_path": registered_path,
1255
+ "transform_path": transform_path,
1256
+ "metrics_before": metrics_before,
1257
+ "metrics_after": metrics_after,
1258
+ "registration_type": "deformable",
1259
+ }
1260
+
1261
+ return results
1262
+
1263
+
1264
+ def batch_register_images(
1265
+ fixed_image_path: str,
1266
+ moving_images_dir: str,
1267
+ output_dir: str,
1268
+ transform_type: str = "rigid",
1269
+ metric: str = "mutual_information",
1270
+ optimizer: str = "gradient_descent",
1271
+ preprocess: bool = True,
1272
+ create_visualizations: bool = True,
1273
+ learning_rate: float = 0.01,
1274
+ number_of_iterations: int = 100,
1275
+ gradient_convergence_tolerance: float = 1e-6,
1276
+ ) -> dict:
1277
+ """
1278
+ Batch registration of multiple images to a single reference.
1279
+
1280
+ Args:
1281
+ fixed_image_path: Path to reference image
1282
+ moving_images_dir: Directory containing images to register
1283
+ output_dir: Directory to save results
1284
+ transform_type: Type of registration ('rigid', 'affine', 'deformable')
1285
+ metric: Similarity metric
1286
+ optimizer: Optimization method
1287
+ preprocess: Whether to preprocess images
1288
+ create_visualizations: Whether to create visualizations
1289
+ learning_rate: Learning rate for optimizer
1290
+ number_of_iterations: Maximum iterations
1291
+ gradient_convergence_tolerance: Convergence tolerance
1292
+
1293
+ Returns:
1294
+ Dictionary with batch registration results
1295
+ """
1296
+ logger.info(f"Starting batch {transform_type} registration...")
1297
+
1298
+ # Find all image files in the directory
1299
+ image_files = []
1300
+ for file in os.listdir(moving_images_dir):
1301
+ if any(file.endswith(ext) for ext in [".nii", ".nii.gz", ".nrrd", ".mha", ".mhd"]):
1302
+ image_files.append(os.path.join(moving_images_dir, file))
1303
+
1304
+ if not image_files:
1305
+ raise ValueError(f"No image files found in {moving_images_dir}")
1306
+
1307
+ logger.info(f"Found {len(image_files)} images to register")
1308
+
1309
+ # Create output directory
1310
+ os.makedirs(output_dir, exist_ok=True)
1311
+
1312
+ # Process each image
1313
+ results = {}
1314
+ ImageRegistrationTool()
1315
+
1316
+ for i, moving_image_path in enumerate(image_files):
1317
+ logger.info(f"Processing {i + 1}/{len(image_files)}: {os.path.basename(moving_image_path)}")
1318
+
1319
+ # Create individual output directory
1320
+ image_name = os.path.splitext(os.path.basename(moving_image_path))[0]
1321
+ if image_name.endswith(".nii"):
1322
+ image_name = os.path.splitext(image_name)[0]
1323
+
1324
+ individual_output_dir = os.path.join(output_dir, f"registration_{image_name}")
1325
+
1326
+ try:
1327
+ if transform_type == "rigid":
1328
+ result = quick_rigid_registration(
1329
+ fixed_image_path,
1330
+ moving_image_path,
1331
+ individual_output_dir,
1332
+ metric,
1333
+ optimizer,
1334
+ preprocess,
1335
+ create_visualizations,
1336
+ learning_rate,
1337
+ number_of_iterations,
1338
+ gradient_convergence_tolerance,
1339
+ )
1340
+ elif transform_type == "affine":
1341
+ result = quick_affine_registration(
1342
+ fixed_image_path,
1343
+ moving_image_path,
1344
+ individual_output_dir,
1345
+ metric,
1346
+ optimizer,
1347
+ preprocess,
1348
+ create_visualizations,
1349
+ learning_rate,
1350
+ number_of_iterations,
1351
+ gradient_convergence_tolerance,
1352
+ )
1353
+ elif transform_type == "deformable":
1354
+ result = quick_deformable_registration(
1355
+ fixed_image_path,
1356
+ moving_image_path,
1357
+ individual_output_dir,
1358
+ metric,
1359
+ optimizer,
1360
+ preprocess,
1361
+ create_visualizations,
1362
+ learning_rate,
1363
+ number_of_iterations,
1364
+ gradient_convergence_tolerance,
1365
+ )
1366
+ else:
1367
+ raise ValueError(f"Unknown transform type: {transform_type}")
1368
+
1369
+ results[image_name] = result
1370
+ logger.info(f"Successfully registered {image_name}")
1371
+
1372
+ except Exception as e:
1373
+ logger.error(f"Failed to register {image_name}: {e}")
1374
+ results[image_name] = {"error": str(e)}
1375
+
1376
+ logger.info(f"Batch registration completed. Processed {len(image_files)} images.")
1377
+ return results
1378
+
1379
+
1380
+ def calculate_similarity_metrics(image1_path: str, image2_path: str) -> dict[str, float]:
1381
+ """
1382
+ Calculate similarity metrics between two images.
1383
+
1384
+ Args:
1385
+ image1_path: Path to first image
1386
+ image2_path: Path to second image
1387
+
1388
+ Returns:
1389
+ Dictionary of similarity metrics
1390
+ """
1391
+ tool = ImageRegistrationTool()
1392
+ image1 = tool.load_image(image1_path)
1393
+ image2 = tool.load_image(image2_path)
1394
+ return tool.calculate_similarity_metrics(image1, image2)
Biomanus_upload/biomni/tool/biophysics.py ADDED
@@ -0,0 +1,513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def predict_protein_disorder_regions(protein_sequence, threshold=0.5, output_file="disorder_prediction_results.csv"):
2
+ """Predicts intrinsically disordered regions (IDRs) in a protein sequence using IUPred2A.
3
+
4
+ Parameters
5
+ ----------
6
+ protein_sequence : str
7
+ The amino acid sequence of the protein to analyze
8
+ threshold : float, optional
9
+ The disorder score threshold above which a residue is considered disordered (default: 0.5)
10
+ output_file : str, optional
11
+ Filename to save the per-residue disorder scores (default: "disorder_prediction_results.csv")
12
+
13
+ Returns
14
+ -------
15
+ str
16
+ A research log summarizing the prediction process and results
17
+
18
+ """
19
+ import csv
20
+ import re
21
+
22
+ import requests
23
+
24
+ # Clean the input sequence
25
+ protein_sequence = "".join(re.findall(r"[A-Za-z]", protein_sequence))
26
+
27
+ # Step 1: Submit the sequence to IUPred2A web server
28
+ url = "https://iupred2a.elte.hu/iupred2a"
29
+ payload = {
30
+ "seq": protein_sequence,
31
+ "iupred2": "long", # Use IUPred2 long disorder prediction
32
+ "anchor2": "no", # Don't use ANCHOR2 prediction
33
+ }
34
+
35
+ try:
36
+ response = requests.post(url, data=payload)
37
+ response.raise_for_status()
38
+ except requests.exceptions.RequestException as e:
39
+ return f"Error accessing IUPred2A server: {str(e)}"
40
+
41
+ # Step 2: Parse the results to extract disorder scores
42
+ result_lines = response.text.split("\n")
43
+ scores = []
44
+
45
+ for line in result_lines:
46
+ if line.startswith("#") or not line.strip():
47
+ continue
48
+ parts = line.split()
49
+ if len(parts) >= 3:
50
+ try:
51
+ position = int(parts[0])
52
+ residue = parts[1]
53
+ score = float(parts[2])
54
+ scores.append((position, residue, score))
55
+ except (ValueError, IndexError):
56
+ continue
57
+
58
+ if not scores:
59
+ return "No valid prediction data was returned from the server."
60
+
61
+ # Step 3: Identify disordered regions
62
+ disordered_regions = []
63
+ current_region = []
64
+
65
+ for pos, _, score in scores:
66
+ if score >= threshold:
67
+ if not current_region:
68
+ current_region = [pos]
69
+ elif pos == current_region[-1] + 1:
70
+ current_region.append(pos)
71
+ else:
72
+ if len(current_region) > 1:
73
+ disordered_regions.append((current_region[0], current_region[-1]))
74
+ current_region = [pos]
75
+ elif current_region and len(current_region) > 1:
76
+ disordered_regions.append((current_region[0], current_region[-1]))
77
+ current_region = []
78
+ elif current_region:
79
+ current_region = []
80
+
81
+ # Add the last region if it exists
82
+ if current_region and len(current_region) > 1:
83
+ disordered_regions.append((current_region[0], current_region[-1]))
84
+
85
+ # Step 4: Save results to CSV
86
+ with open(output_file, "w", newline="") as f:
87
+ writer = csv.writer(f)
88
+ writer.writerow(["Position", "Amino_Acid", "Disorder_Score", "Is_Disordered"])
89
+ for pos, aa, score in scores:
90
+ is_disordered = "Yes" if score >= threshold else "No"
91
+ writer.writerow([pos, aa, score, is_disordered])
92
+
93
+ # Step 5: Generate the research log
94
+ total_residues = len(scores)
95
+ disordered_count = sum(1 for _, _, score in scores if score >= threshold)
96
+ disordered_percentage = (disordered_count / total_residues) * 100 if total_residues > 0 else 0
97
+
98
+ log = f"""
99
+ Intrinsically Disordered Region (IDR) Prediction Research Log:
100
+ =============================================================
101
+ Analysis performed using IUPred2A algorithm (long disorder mode)
102
+ Protein sequence length: {total_residues} amino acids
103
+ Disorder threshold: {threshold}
104
+
105
+ Results Summary:
106
+ - {disordered_count} residues ({disordered_percentage:.2f}%) predicted as disordered
107
+ - {len(disordered_regions)} distinct disordered regions identified
108
+
109
+ Disordered Regions:
110
+ """
111
+
112
+ if disordered_regions:
113
+ for start, end in disordered_regions:
114
+ length = end - start + 1
115
+ log += f"- Region {start}-{end} (length: {length} residues)\n"
116
+ else:
117
+ log += "- No significant disordered regions found\n"
118
+
119
+ log += f"\nDetailed per-residue scores saved to: {output_file}"
120
+
121
+ return log
122
+
123
+
124
+ def analyze_cell_morphology_and_cytoskeleton(image_path, output_dir="./results", threshold_method="otsu"):
125
+ """Quantifies cell morphology and cytoskeletal organization from fluorescence microscopy images.
126
+
127
+ Parameters
128
+ ----------
129
+ image_path : str
130
+ Path to the fluorescence microscopy image file
131
+ output_dir : str, optional
132
+ Directory to save output files (default: './results')
133
+ threshold_method : str, optional
134
+ Method for cell segmentation ('otsu', 'adaptive', or 'manual') (default: 'otsu')
135
+
136
+ Returns
137
+ -------
138
+ str
139
+ Research log summarizing the analysis steps and results
140
+
141
+ """
142
+ import os
143
+ from datetime import datetime
144
+
145
+ import cv2
146
+ import numpy as np
147
+ import pandas as pd
148
+ from skimage import exposure, feature, filters, io, measure, morphology
149
+ from skimage.color import rgb2gray
150
+
151
+ # Create output directory if it doesn't exist
152
+ os.makedirs(output_dir, exist_ok=True)
153
+
154
+ # Start log
155
+ log = f"Cell Morphology and Cytoskeleton Analysis Log - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
156
+ log += f"Analyzing image: {image_path}\n\n"
157
+
158
+ # Load image
159
+ log += "Step 1: Loading and preprocessing image\n"
160
+ try:
161
+ image = io.imread(image_path)
162
+ # Convert to grayscale if RGB
163
+ if len(image.shape) > 2:
164
+ gray_image = rgb2gray(image)
165
+ log += "- Converted RGB image to grayscale\n"
166
+ else:
167
+ gray_image = image
168
+
169
+ # Enhance contrast
170
+ gray_image = exposure.equalize_hist(gray_image)
171
+ log += "- Enhanced image contrast\n"
172
+ except Exception as e:
173
+ return f"Error loading image: {str(e)}"
174
+
175
+ # Segment cells
176
+ log += "\nStep 2: Segmenting cells from background\n"
177
+ if threshold_method == "otsu":
178
+ thresh = filters.threshold_otsu(gray_image)
179
+ binary = gray_image > thresh
180
+ log += f"- Applied Otsu thresholding (threshold value: {thresh:.4f})\n"
181
+ elif threshold_method == "adaptive":
182
+ binary = filters.threshold_local(gray_image, block_size=35, offset=0.05)
183
+ binary = gray_image > binary
184
+ log += "- Applied adaptive thresholding\n"
185
+ else: # manual
186
+ thresh = 0.5 # Default value, can be parameterized
187
+ binary = gray_image > thresh
188
+ log += f"- Applied manual thresholding (threshold value: {thresh:.4f})\n"
189
+
190
+ # Clean up binary image
191
+ binary = morphology.remove_small_objects(binary, min_size=100)
192
+ binary = morphology.remove_small_holes(binary, area_threshold=100)
193
+ binary = morphology.binary_closing(binary, morphology.disk(3))
194
+ log += "- Applied morphological operations to clean segmentation\n"
195
+
196
+ # Label cells
197
+ labeled_cells, num_cells = measure.label(binary, return_num=True)
198
+ log += f"- Identified {num_cells} cell regions\n"
199
+
200
+ # Analyze cell properties
201
+ log += "\nStep 3: Analyzing cell morphology\n"
202
+ cell_props = measure.regionprops_table(
203
+ labeled_cells,
204
+ gray_image,
205
+ properties=(
206
+ "area",
207
+ "perimeter",
208
+ "major_axis_length",
209
+ "minor_axis_length",
210
+ "eccentricity",
211
+ "orientation",
212
+ "solidity",
213
+ ),
214
+ )
215
+
216
+ # Convert to DataFrame for easier manipulation
217
+ cell_df = pd.DataFrame(cell_props)
218
+
219
+ # Calculate additional metrics
220
+ if len(cell_df) > 0:
221
+ cell_df["aspect_ratio"] = cell_df["major_axis_length"] / cell_df["minor_axis_length"]
222
+ cell_df["circularity"] = (4 * np.pi * cell_df["area"]) / (cell_df["perimeter"] ** 2)
223
+
224
+ # Summary statistics
225
+ log += f"- Average cell area: {cell_df['area'].mean():.2f} pixels\n"
226
+ log += f"- Average aspect ratio: {cell_df['aspect_ratio'].mean():.2f}\n"
227
+ log += f"- Average circularity: {cell_df['circularity'].mean():.2f}\n"
228
+ log += f"- Average eccentricity: {cell_df['eccentricity'].mean():.2f}\n"
229
+ else:
230
+ log += "- No cells detected for morphological analysis\n"
231
+
232
+ # Analyze cytoskeletal organization
233
+ log += "\nStep 4: Analyzing cytoskeletal organization\n"
234
+
235
+ # Edge detection to highlight cytoskeletal fibers
236
+ edges = feature.canny(gray_image, sigma=2)
237
+
238
+ # Use Hough transform to detect lines (cytoskeletal fibers)
239
+ if np.any(edges):
240
+ lines = cv2.HoughLinesP(
241
+ edges.astype(np.uint8),
242
+ 1,
243
+ np.pi / 180,
244
+ threshold=10,
245
+ minLineLength=10,
246
+ maxLineGap=5,
247
+ )
248
+
249
+ if lines is not None:
250
+ # Calculate line orientations
251
+ orientations = []
252
+ for line in lines:
253
+ x1, y1, x2, y2 = line[0]
254
+ if x2 - x1 != 0: # Avoid division by zero
255
+ angle = np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi
256
+ orientations.append(angle)
257
+
258
+ if orientations:
259
+ # Convert to numpy array for calculations
260
+ orientations = np.array(orientations)
261
+
262
+ # Calculate alignment metrics
263
+ mean_orientation = np.mean(orientations)
264
+ # Normalize angles to -90 to 90 degrees
265
+ norm_angles = np.mod(orientations + 90, 180) - 90
266
+ std_orientation = np.std(norm_angles)
267
+
268
+ # Order parameter (measure of alignment, 1 = perfectly aligned, 0 = random)
269
+ # Convert angles to radians for calculation
270
+ rad_angles = np.radians(norm_angles)
271
+ order_parameter = np.sqrt(np.mean(np.cos(2 * rad_angles)) ** 2 + np.mean(np.sin(2 * rad_angles)) ** 2)
272
+
273
+ log += f"- Detected {len(orientations)} cytoskeletal fibers\n"
274
+ log += f"- Mean fiber orientation: {mean_orientation:.2f} degrees\n"
275
+ log += f"- Standard deviation of orientation: {std_orientation:.2f} degrees\n"
276
+ log += f"- Order parameter (alignment): {order_parameter:.4f} (0=random, 1=aligned)\n"
277
+
278
+ # Add fiber data to dataframe
279
+ fiber_df = pd.DataFrame({"fiber_orientation": orientations})
280
+ else:
281
+ log += "- No fiber orientations could be calculated\n"
282
+ fiber_df = pd.DataFrame()
283
+ else:
284
+ log += "- No cytoskeletal fibers detected\n"
285
+ fiber_df = pd.DataFrame()
286
+ else:
287
+ log += "- No edges detected for cytoskeletal analysis\n"
288
+ fiber_df = pd.DataFrame()
289
+
290
+ # Save results
291
+ log += "\nStep 5: Saving results\n"
292
+
293
+ # Save cell morphology data
294
+ if len(cell_df) > 0:
295
+ cell_csv_path = os.path.join(output_dir, "cell_morphology_data.csv")
296
+ cell_df.to_csv(cell_csv_path, index=False)
297
+ log += f"- Cell morphology data saved to: {cell_csv_path}\n"
298
+
299
+ # Save fiber orientation data
300
+ if len(fiber_df) > 0:
301
+ fiber_csv_path = os.path.join(output_dir, "fiber_orientation_data.csv")
302
+ fiber_df.to_csv(fiber_csv_path, index=False)
303
+ log += f"- Fiber orientation data saved to: {fiber_csv_path}\n"
304
+
305
+ # Save segmentation image
306
+ if num_cells > 0:
307
+ segmentation_path = os.path.join(output_dir, "cell_segmentation.png")
308
+ io.imsave(segmentation_path, labeled_cells.astype(np.uint8) * 50)
309
+ log += f"- Cell segmentation image saved to: {segmentation_path}\n"
310
+
311
+ # Summary
312
+ log += "\nAnalysis Summary:\n"
313
+ log += f"- Processed image: {image_path}\n"
314
+ log += f"- Detected {num_cells} cells\n"
315
+ if len(cell_df) > 0:
316
+ log += f"- Cell size range: {cell_df['area'].min():.1f} to {cell_df['area'].max():.1f} pixels\n"
317
+ log += f"- Cell shape: average aspect ratio = {cell_df['aspect_ratio'].mean():.2f}\n"
318
+ if "order_parameter" in locals():
319
+ log += f"- Cytoskeletal organization: alignment parameter = {order_parameter:.4f}\n"
320
+
321
+ return log
322
+
323
+
324
+ def analyze_tissue_deformation_flow(image_sequence, output_dir="results", pixel_scale=1.0):
325
+ """Quantify tissue deformation and flow dynamics from microscopy image sequence.
326
+
327
+ Parameters
328
+ ----------
329
+ image_sequence : list or numpy.ndarray
330
+ Sequence of microscopy images (either a list of file paths or a 3D numpy array [time, height, width])
331
+ output_dir : str, optional
332
+ Directory to save results (default: "results")
333
+ pixel_scale : float, optional
334
+ Physical scale of pixels (e.g., μm/pixel) for proper scaling of metrics (default: 1.0)
335
+
336
+ Returns
337
+ -------
338
+ str
339
+ Research log summarizing the analysis steps and results
340
+
341
+ """
342
+ import os
343
+ from datetime import datetime
344
+
345
+ import cv2
346
+ import numpy as np
347
+
348
+ # Create output directory if it doesn't exist
349
+ os.makedirs(output_dir, exist_ok=True)
350
+
351
+ # Load images if paths are provided
352
+ if isinstance(image_sequence[0], str):
353
+ loaded_images = []
354
+ for img_path in image_sequence:
355
+ img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
356
+ if img is None:
357
+ return f"Error: Could not load image {img_path}"
358
+ loaded_images.append(img)
359
+ frames = np.array(loaded_images)
360
+ else:
361
+ frames = image_sequence
362
+ # Convert to grayscale if needed
363
+ if len(frames.shape) > 3: # Has color channels
364
+ frames = np.array(
365
+ [cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) if frame.shape[-1] == 3 else frame for frame in frames]
366
+ )
367
+
368
+ # Parameters for optical flow
369
+ lk_params = {
370
+ "winSize": (15, 15),
371
+ "maxLevel": 2,
372
+ "criteria": (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03),
373
+ }
374
+
375
+ # Metrics storage
376
+ num_frames = len(frames)
377
+ flow_fields = []
378
+ divergence_maps = []
379
+ curl_maps = []
380
+ strain_maps = []
381
+
382
+ # Log initialization
383
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
384
+ log = f"Tissue Deformation and Flow Analysis - {timestamp}\n"
385
+ log += f"Number of frames analyzed: {num_frames}\n"
386
+ log += f"Image dimensions: {frames[0].shape[0]}x{frames[0].shape[1]} pixels\n"
387
+ log += f"Pixel scale: {pixel_scale} units/pixel\n\n"
388
+ log += "Analysis Steps:\n"
389
+
390
+ # Create feature points grid (evenly spaced points)
391
+ y, x = np.mgrid[0 : frames[0].shape[0] : 20, 0 : frames[0].shape[1] : 20]
392
+ feature_points = np.stack((x.flatten(), y.flatten()), axis=1).astype(np.float32)
393
+
394
+ for i in range(num_frames - 1):
395
+ log += f"Processing frame pair {i} and {i + 1}...\n"
396
+
397
+ # Calculate optical flow using Lucas-Kanade
398
+ prev_frame = frames[i]
399
+ next_frame = frames[i + 1]
400
+
401
+ # Compute flow for the grid points
402
+ next_points, status, _ = cv2.calcOpticalFlowPyrLK(prev_frame, next_frame, feature_points, None, **lk_params)
403
+
404
+ # Filter valid points
405
+ valid_idx = status.flatten() == 1
406
+ valid_prev_points = feature_points[valid_idx]
407
+ valid_next_points = next_points[valid_idx]
408
+
409
+ # Calculate displacement vectors
410
+ displacement = valid_next_points - valid_prev_points
411
+ points = valid_prev_points
412
+
413
+ # Interpolate flow field to full image resolution
414
+ flow_field = np.zeros((frames[0].shape[0], frames[0].shape[1], 2), dtype=np.float32)
415
+
416
+ # Simple nearest-neighbor interpolation for demonstration
417
+ # In a production system, you might use more sophisticated interpolation
418
+ for j, (x, y) in enumerate(points.astype(int)):
419
+ if 0 <= y < flow_field.shape[0] and 0 <= x < flow_field.shape[1]:
420
+ flow_field[y, x] = displacement[j]
421
+
422
+ # Calculate derivatives for deformation analysis
423
+ u = flow_field[:, :, 0] # x-component of flow
424
+ v = flow_field[:, :, 1] # y-component of flow
425
+
426
+ # Calculate divergence (expansion/contraction)
427
+ # Using central differences for derivatives
428
+ u_x = cv2.Sobel(u, cv2.CV_64F, 1, 0, ksize=3) / (8.0 * pixel_scale)
429
+ v_y = cv2.Sobel(v, cv2.CV_64F, 0, 1, ksize=3) / (8.0 * pixel_scale)
430
+ divergence = u_x + v_y
431
+
432
+ # Calculate curl (rotation)
433
+ u_y = cv2.Sobel(u, cv2.CV_64F, 0, 1, ksize=3) / (8.0 * pixel_scale)
434
+ v_x = cv2.Sobel(v, cv2.CV_64F, 1, 0, ksize=3) / (8.0 * pixel_scale)
435
+ curl = v_x - u_y
436
+
437
+ # Calculate strain tensor components
438
+ strain_xx = u_x
439
+ strain_yy = v_y
440
+ strain_xy = 0.5 * (u_y + v_x)
441
+
442
+ # Magnitude of strain tensor (Frobenius norm)
443
+ strain_magnitude = np.sqrt(strain_xx**2 + strain_yy**2 + 2 * strain_xy**2)
444
+
445
+ # Store results
446
+ flow_fields.append(flow_field)
447
+ divergence_maps.append(divergence)
448
+ curl_maps.append(curl)
449
+ strain_maps.append(strain_magnitude)
450
+
451
+ # Visualize flow field
452
+ flow_viz = np.zeros((frames[0].shape[0], frames[0].shape[1], 3), dtype=np.uint8)
453
+ flow_viz[..., 0] = next_frame # Use next frame as background
454
+ flow_viz[..., 1] = next_frame
455
+ flow_viz[..., 2] = next_frame
456
+
457
+ # Draw flow vectors for visualization
458
+ step = 20
459
+ for y in range(0, flow_field.shape[0], step):
460
+ for x in range(0, flow_field.shape[1], step):
461
+ dx, dy = flow_field[y, x]
462
+ if abs(dx) > 0.5 or abs(dy) > 0.5: # Only draw significant flow
463
+ cv2.arrowedLine(
464
+ flow_viz,
465
+ (x, y),
466
+ (int(x + dx), int(y + dy)),
467
+ (0, 255, 0), # Green
468
+ 1,
469
+ tipLength=0.3,
470
+ )
471
+
472
+ # Save visualizations
473
+ flow_viz_path = os.path.join(output_dir, f"flow_viz_{i:03d}.png")
474
+ divergence_path = os.path.join(output_dir, f"divergence_{i:03d}.npy")
475
+ curl_path = os.path.join(output_dir, f"curl_{i:03d}.npy")
476
+ strain_path = os.path.join(output_dir, f"strain_{i:03d}.npy")
477
+
478
+ cv2.imwrite(flow_viz_path, flow_viz)
479
+ np.save(divergence_path, divergence)
480
+ np.save(curl_path, curl)
481
+ np.save(strain_path, strain_magnitude)
482
+
483
+ # Calculate summary statistics
484
+ mean_divergence = np.mean([np.mean(div) for div in divergence_maps])
485
+ max_divergence = np.max([np.max(div) for div in divergence_maps])
486
+ mean_curl = np.mean([np.mean(np.abs(c)) for c in curl_maps])
487
+ mean_strain = np.mean([np.mean(s) for s in strain_maps])
488
+
489
+ # Add summary to log
490
+ log += "\nAnalysis Results:\n"
491
+ log += f"Mean tissue divergence: {mean_divergence:.6f} (expansion/contraction rate)\n"
492
+ log += f"Maximum divergence: {max_divergence:.6f}\n"
493
+ log += f"Mean absolute curl: {mean_curl:.6f} (rotation rate)\n"
494
+ log += f"Mean strain magnitude: {mean_strain:.6f} (deformation intensity)\n\n"
495
+
496
+ # Save summary data
497
+ summary_data = {
498
+ "mean_divergence": mean_divergence,
499
+ "max_divergence": max_divergence,
500
+ "mean_curl": mean_curl,
501
+ "mean_strain": mean_strain,
502
+ }
503
+ summary_path = os.path.join(output_dir, "deformation_summary.npy")
504
+ np.save(summary_path, summary_data)
505
+
506
+ log += "Files Generated:\n"
507
+ log += f"- Flow visualization images: {output_dir}/flow_viz_*.png\n"
508
+ log += f"- Divergence maps: {output_dir}/divergence_*.npy\n"
509
+ log += f"- Curl maps: {output_dir}/curl_*.npy\n"
510
+ log += f"- Strain maps: {output_dir}/strain_*.npy\n"
511
+ log += f"- Summary statistics: {output_dir}/deformation_summary.npy\n"
512
+
513
+ return log
Biomanus_upload/biomni/tool/cancer_biology.py ADDED
@@ -0,0 +1,1292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def analyze_ddr_network_in_cancer(expression_data_path, mutation_data_path, output_dir="./results"):
2
+ """Analyze DNA Damage Response (DDR) network alterations and dependencies in cancer samples.
3
+
4
+ This function reconstructs the DDR network from genomic data, identifies disruptions
5
+ in the network, and analyzes dependencies between DDR pathway components in cancer.
6
+
7
+ Parameters
8
+ ----------
9
+ expression_data_path : str
10
+ Path to gene expression data file (CSV format with genes as rows, samples as columns)
11
+ mutation_data_path : str
12
+ Path to mutation data file (CSV format with genes as rows, samples as columns,
13
+ values indicating mutation status)
14
+ output_dir : str, optional
15
+ Directory to save output files (default: "./results")
16
+
17
+ Returns
18
+ -------
19
+ str
20
+ Research log summarizing the DDR network analysis, findings about disruptions,
21
+ and potential therapeutic vulnerabilities
22
+
23
+ """
24
+ import os
25
+ from datetime import datetime
26
+
27
+ import gseapy as gp
28
+ import networkx as nx
29
+ import pandas as pd
30
+ from scipy.stats import pearsonr
31
+
32
+ # Create output directory if it doesn't exist
33
+ os.makedirs(output_dir, exist_ok=True)
34
+
35
+ research_log = f"DDR Network Analysis Research Log - {datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
36
+ research_log += "=" * 80 + "\n\n"
37
+
38
+ # Step 1: Load and preprocess genomic data
39
+ research_log += "STEP 1: Loading and preprocessing genomic data\n"
40
+
41
+ try:
42
+ expr_data = pd.read_csv(expression_data_path, index_col=0)
43
+ mut_data = pd.read_csv(mutation_data_path, index_col=0)
44
+
45
+ research_log += f"- Loaded expression data: {expr_data.shape[0]} genes × {expr_data.shape[1]} samples\n"
46
+ research_log += f"- Loaded mutation data: {mut_data.shape[0]} genes × {mut_data.shape[1]} samples\n"
47
+
48
+ # Define core DDR genes (based on literature)
49
+ ddr_genes = [
50
+ # DNA damage sensors
51
+ "ATM",
52
+ "ATR",
53
+ "PRKDC",
54
+ "RAD50",
55
+ "MRE11",
56
+ "NBN",
57
+ # Signal transducers
58
+ "CHEK1",
59
+ "CHEK2",
60
+ "TP53",
61
+ "BRCA1",
62
+ "BRCA2",
63
+ "MDC1",
64
+ "H2AX",
65
+ # Effectors - Homologous Recombination
66
+ "RAD51",
67
+ "RAD52",
68
+ "PALB2",
69
+ "RAD54L",
70
+ # Effectors - Non-Homologous End Joining
71
+ "XRCC4",
72
+ "LIG4",
73
+ "XRCC5",
74
+ "XRCC6",
75
+ # Effectors - Base Excision Repair
76
+ "PARP1",
77
+ "APEX1",
78
+ "OGG1",
79
+ "XRCC1",
80
+ # Effectors - Nucleotide Excision Repair
81
+ "XPA",
82
+ "XPC",
83
+ "ERCC1",
84
+ "ERCC2",
85
+ "ERCC3",
86
+ "ERCC4",
87
+ "ERCC5",
88
+ # Effectors - Mismatch Repair
89
+ "MLH1",
90
+ "MSH2",
91
+ "MSH6",
92
+ "PMS2",
93
+ ]
94
+
95
+ # Filter expression and mutation data for DDR genes
96
+ ddr_expr = expr_data.loc[expr_data.index.isin(ddr_genes)]
97
+ ddr_mut = mut_data.loc[mut_data.index.isin(ddr_genes)]
98
+
99
+ research_log += f"- Filtered data for {len(ddr_genes)} DDR pathway genes\n"
100
+ research_log += f"- Found {ddr_expr.shape[0]} DDR genes in expression data\n"
101
+ research_log += f"- Found {ddr_mut.shape[0]} DDR genes in mutation data\n\n"
102
+
103
+ except Exception as e:
104
+ research_log += f"Error in data loading: {str(e)}\n\n"
105
+ return research_log
106
+
107
+ # Step 2: Network reconstruction
108
+ research_log += "STEP 2: Reconstructing DDR gene network\n"
109
+
110
+ # Create correlation-based network
111
+ G = nx.Graph()
112
+
113
+ # Add nodes (genes)
114
+ for gene in ddr_expr.index:
115
+ # Add node with mutation frequency information
116
+ mutation_freq = ddr_mut.loc[gene].mean() if gene in ddr_mut.index else 0
117
+
118
+ G.add_node(gene, mutation_freq=mutation_freq)
119
+
120
+ # Add edges based on gene expression correlation
121
+ for i, gene1 in enumerate(ddr_expr.index):
122
+ for gene2 in ddr_expr.index[i + 1 :]:
123
+ corr, p_value = pearsonr(ddr_expr.loc[gene1], ddr_expr.loc[gene2])
124
+ if abs(corr) > 0.4 and p_value < 0.05: # Significant correlation threshold
125
+ G.add_edge(gene1, gene2, weight=abs(corr), correlation=corr)
126
+
127
+ research_log += f"- Constructed network with {G.number_of_nodes()} nodes and {G.number_of_edges()} edges\n"
128
+
129
+ # Save network for visualization
130
+ network_file = os.path.join(output_dir, "ddr_network.graphml")
131
+ nx.write_graphml(G, network_file)
132
+ research_log += f"- Network saved to: {network_file}\n\n"
133
+
134
+ # Step 3: Network analysis
135
+ research_log += "STEP 3: Analyzing DDR network properties\n"
136
+
137
+ # Calculate network centrality measures
138
+ degree_centrality = nx.degree_centrality(G)
139
+ betweenness_centrality = nx.betweenness_centrality(G)
140
+
141
+ # Identify hub genes (high degree centrality)
142
+ hub_genes = sorted(degree_centrality.items(), key=lambda x: x[1], reverse=True)[:5]
143
+
144
+ research_log += "- Top 5 hub genes (highest connectivity):\n"
145
+ for gene, centrality in hub_genes:
146
+ research_log += f" * {gene}: {centrality:.4f}\n"
147
+
148
+ # Identify bottleneck genes (high betweenness centrality)
149
+ bottleneck_genes = sorted(betweenness_centrality.items(), key=lambda x: x[1], reverse=True)[:5]
150
+
151
+ research_log += "- Top 5 bottleneck genes (critical for information flow):\n"
152
+ for gene, centrality in bottleneck_genes:
153
+ research_log += f" * {gene}: {centrality:.4f}\n"
154
+
155
+ # Identify frequently mutated DDR genes
156
+ mutation_freq = {node: G.nodes[node]["mutation_freq"] for node in G.nodes()}
157
+ frequently_mutated = sorted(mutation_freq.items(), key=lambda x: x[1], reverse=True)[:5]
158
+
159
+ research_log += "- Top 5 frequently mutated DDR genes:\n"
160
+ for gene, freq in frequently_mutated:
161
+ research_log += f" * {gene}: {freq:.4f}\n\n"
162
+
163
+ # Step 4: Community detection to identify DDR sub-pathways
164
+ research_log += "STEP 4: Identifying DDR sub-pathways through community detection\n"
165
+
166
+ # Use Louvain method for community detection
167
+ try:
168
+ from community import best_partition
169
+
170
+ partition = best_partition(G)
171
+ communities = {}
172
+ for node, community_id in partition.items():
173
+ if community_id not in communities:
174
+ communities[community_id] = []
175
+ communities[community_id].append(node)
176
+
177
+ research_log += f"- Identified {len(communities)} DDR sub-pathways\n"
178
+
179
+ for i, (_, genes) in enumerate(communities.items()):
180
+ research_log += f" * Sub-pathway {i + 1}: {', '.join(genes)}\n"
181
+
182
+ except ImportError:
183
+ research_log += "- Community detection skipped (python-louvain package not installed)\n"
184
+
185
+ # Step 5: Identify disrupted DDR pathways using GSEA
186
+ research_log += "\nSTEP 5: Pathway enrichment analysis of DDR genes\n"
187
+
188
+ try:
189
+ # Create gene list for GSEA
190
+ gene_list = ddr_expr.mean(axis=1).sort_values(ascending=False)
191
+
192
+ # Run GSEA with GO Biological Process
193
+ enrichr_results = gp.enrichr(
194
+ gene_list=gene_list.index.tolist(),
195
+ gene_sets=["GO_Biological_Process_2021"],
196
+ outdir=os.path.join(output_dir, "enrichr_output"),
197
+ cutoff=0.5,
198
+ )
199
+
200
+ # Filter for DNA repair related terms
201
+ ddr_terms = [
202
+ term
203
+ for term in enrichr_results.results["Term"]
204
+ if any(x in term.lower() for x in ["dna repair", "damage", "recombination", "checkpoint"])
205
+ ]
206
+
207
+ if ddr_terms:
208
+ research_log += "- Enriched DDR-related pathways:\n"
209
+ for term in ddr_terms[:5]: # Top 5 DDR-related terms
210
+ research_log += f" * {term}\n"
211
+ else:
212
+ research_log += "- No significant DDR pathway enrichment found\n"
213
+
214
+ # Save full enrichment results
215
+ enrichment_file = os.path.join(output_dir, "ddr_pathway_enrichment.csv")
216
+ enrichr_results.results.to_csv(enrichment_file)
217
+ research_log += f"- Full enrichment results saved to: {enrichment_file}\n\n"
218
+
219
+ except Exception as e:
220
+ research_log += f"- Pathway enrichment analysis error: {str(e)}\n\n"
221
+
222
+ # Step 6: Summary of findings
223
+ research_log += "SUMMARY OF FINDINGS\n"
224
+ research_log += "=" * 80 + "\n"
225
+
226
+ # Key hub genes (potential therapeutic targets)
227
+ research_log += "1. Key DDR hub genes that may serve as potential therapeutic targets:\n"
228
+ for gene, _ in hub_genes[:3]:
229
+ research_log += f" - {gene}\n"
230
+
231
+ # Frequently mutated genes
232
+ research_log += "\n2. Frequently mutated DDR genes in the samples:\n"
233
+ for gene, freq in frequently_mutated[:3]:
234
+ research_log += f" - {gene} (mutation frequency: {freq:.2%})\n"
235
+
236
+ # Network structure insights
237
+ research_log += "\n3. DDR network structure insights:\n"
238
+ research_log += f" - Network density: {nx.density(G):.4f}\n"
239
+
240
+ try:
241
+ avg_clustering = nx.average_clustering(G)
242
+ research_log += f" - Average clustering coefficient: {avg_clustering:.4f}\n"
243
+ except Exception:
244
+ pass
245
+
246
+ research_log += "\n4. Potential DDR dependencies and synthetic lethality targets:\n"
247
+ # Identify potential synthetic lethality pairs (genes with high correlation and one is frequently mutated)
248
+ synthetic_lethality_candidates = []
249
+ for gene1, gene2, data in G.edges(data=True):
250
+ if abs(data["correlation"]) > 0.6: # Strong correlation
251
+ mut_freq1 = G.nodes[gene1]["mutation_freq"]
252
+ mut_freq2 = G.nodes[gene2]["mutation_freq"]
253
+ if (mut_freq1 > 0.1 and mut_freq2 < 0.05) or (mut_freq2 > 0.1 and mut_freq1 < 0.05):
254
+ if mut_freq1 > mut_freq2:
255
+ synthetic_lethality_candidates.append((gene1, gene2, data["correlation"], mut_freq1))
256
+ else:
257
+ synthetic_lethality_candidates.append((gene2, gene1, data["correlation"], mut_freq2))
258
+
259
+ if synthetic_lethality_candidates:
260
+ for mutated, target, _, _ in sorted(synthetic_lethality_candidates, key=lambda x: x[3], reverse=True)[:3]:
261
+ research_log += f" - {mutated} (frequently mutated) and {target} (potential dependency)\n"
262
+ else:
263
+ research_log += " - No strong synthetic lethality candidates identified\n"
264
+
265
+ return research_log
266
+
267
+
268
+ def analyze_cell_senescence_and_apoptosis(fcs_file_path):
269
+ """Analyze flow cytometry data to quantify senescent and apoptotic cell populations.
270
+
271
+ Parameters
272
+ ----------
273
+ fcs_file_path : str
274
+ Path to the FCS file containing flow cytometry data with measurements for
275
+ senescence-associated β-galactosidase (SA-β-Gal) and Annexin V/7-AAD staining
276
+
277
+ Returns
278
+ -------
279
+ str
280
+ A research log summarizing the analysis steps and results, including percentages
281
+ of senescent and apoptotic cell populations
282
+
283
+ """
284
+ import os
285
+
286
+ import numpy as np
287
+ from FlowCytometryTools import FCMeasurement
288
+
289
+ log = "# Flow Cytometry Analysis of Cell Senescence and Apoptosis\n\n"
290
+
291
+ try:
292
+ # Step 1: Load the FCS file
293
+ log += "## Step 1: Loading Flow Cytometry Data\n"
294
+ log += f"- Loading FCS file from: {fcs_file_path}\n"
295
+
296
+ sample = FCMeasurement(ID="Sample", datafile=fcs_file_path)
297
+ log += f"- Successfully loaded data with {len(sample)} events\n"
298
+ log += f"- Available channels: {', '.join(sample.channel_names)}\n\n"
299
+
300
+ # Step 2: Apply compensation if needed (assuming pre-compensated data)
301
+ log += "## Step 2: Data Preprocessing\n"
302
+ log += "- Checking for outliers and debris based on forward/side scatter\n"
303
+
304
+ # Basic filtering to remove debris based on FSC and SSC
305
+ # Assuming FSC-A and SSC-A are the channel names (adjust if different)
306
+ fsc_channel = "FSC-A" if "FSC-A" in sample.channel_names else sample.channel_names[0]
307
+ ssc_channel = "SSC-A" if "SSC-A" in sample.channel_names else sample.channel_names[1]
308
+
309
+ # Filter out debris (low FSC and SSC)
310
+ sample_filtered = sample.gate(f"{fsc_channel} > 10000 and {ssc_channel} > 5000")
311
+
312
+ log += f"- Filtered out debris: {len(sample)} → {len(sample_filtered)} events ({len(sample_filtered) / len(sample) * 100:.1f}%)\n\n"
313
+
314
+ # Step 3: Identify senescent cells (SA-β-Gal positive)
315
+ log += "## Step 3: Identifying Senescent Cells (SA-β-Gal+)\n"
316
+
317
+ # Find SA-β-Gal channel (adjust based on actual channel name)
318
+ sa_bgal_channel = None
319
+ for channel in sample_filtered.channel_names:
320
+ if "GAL" in channel.upper() or "FITC" in channel.upper():
321
+ sa_bgal_channel = channel
322
+ break
323
+
324
+ if not sa_bgal_channel:
325
+ log += "- WARNING: Could not identify SA-β-Gal channel. Using first fluorescence channel as placeholder.\n"
326
+ # Use the first fluorescence channel as a fallback
327
+ for channel in sample_filtered.channel_names:
328
+ if any(x in channel.upper() for x in ["FL", "BL", "FITC", "PE", "APC"]):
329
+ sa_bgal_channel = channel
330
+ break
331
+
332
+ log += f"- Using {sa_bgal_channel} as SA-β-Gal activity indicator\n"
333
+
334
+ # Determine threshold for SA-β-Gal positivity (using a simple percentile approach)
335
+ # In practice, this would be based on controls or known thresholds
336
+ sa_bgal_threshold = np.percentile(sample_filtered.data[sa_bgal_channel], 80)
337
+ senescent_cells = sample_filtered.gate(f"{sa_bgal_channel} > {sa_bgal_threshold}")
338
+
339
+ senescent_percentage = (len(senescent_cells) / len(sample_filtered)) * 100
340
+ log += f"- Applied threshold at {sa_bgal_threshold:.1f} fluorescence intensity\n"
341
+ log += f"- Identified {len(senescent_cells)} senescent cells ({senescent_percentage:.2f}%)\n\n"
342
+
343
+ # Step 4: Identify apoptotic cells (Annexin V+/7-AAD+)
344
+ log += "## Step 4: Identifying Apoptotic Cells (Annexin V/7-AAD)\n"
345
+
346
+ # Find Annexin V and 7-AAD channels (adjust based on actual channel names)
347
+ annexin_channel = None
348
+ aad_channel = None
349
+
350
+ for channel in sample_filtered.channel_names:
351
+ if "ANNEXIN" in channel.upper() or "PE" in channel.upper():
352
+ annexin_channel = channel
353
+ if "7AAD" in channel.upper() or "AAD" in channel.upper() or "PerCP" in channel.upper():
354
+ aad_channel = channel
355
+
356
+ if not annexin_channel or not aad_channel:
357
+ log += "- WARNING: Could not identify Annexin V and/or 7-AAD channels. Using placeholder channels.\n"
358
+ # Use fallback channels
359
+ fluorescence_channels = [
360
+ ch
361
+ for ch in sample_filtered.channel_names
362
+ if any(x in ch.upper() for x in ["FL", "BL", "FITC", "PE", "APC", "PerCP"])
363
+ ]
364
+ if len(fluorescence_channels) >= 2:
365
+ if not annexin_channel:
366
+ annexin_channel = fluorescence_channels[0]
367
+ if not aad_channel:
368
+ aad_channel = fluorescence_channels[1]
369
+
370
+ log += f"- Using {annexin_channel} as Annexin V indicator\n"
371
+ log += f"- Using {aad_channel} as 7-AAD indicator\n"
372
+
373
+ # Determine thresholds for Annexin V and 7-AAD (using simple percentile approach)
374
+ annexin_threshold = np.percentile(sample_filtered.data[annexin_channel], 90)
375
+ aad_threshold = np.percentile(sample_filtered.data[aad_channel], 90)
376
+
377
+ # Early apoptotic: Annexin V+ / 7-AAD-
378
+ early_apoptotic = sample_filtered.gate(
379
+ f"{annexin_channel} > {annexin_threshold} and {aad_channel} < {aad_threshold}"
380
+ )
381
+ early_apoptotic_percentage = (len(early_apoptotic) / len(sample_filtered)) * 100
382
+
383
+ # Late apoptotic/necrotic: Annexin V+ / 7-AAD+
384
+ late_apoptotic = sample_filtered.gate(
385
+ f"{annexin_channel} > {annexin_threshold} and {aad_channel} > {aad_threshold}"
386
+ )
387
+ late_apoptotic_percentage = (len(late_apoptotic) / len(sample_filtered)) * 100
388
+
389
+ # Total apoptotic (early + late)
390
+ total_apoptotic_percentage = early_apoptotic_percentage + late_apoptotic_percentage
391
+
392
+ log += f"- Early apoptotic cells (Annexin V+/7-AAD-): {early_apoptotic_percentage:.2f}%\n"
393
+ log += f"- Late apoptotic cells (Annexin V+/7-AAD+): {late_apoptotic_percentage:.2f}%\n"
394
+ log += f"- Total apoptotic cells: {total_apoptotic_percentage:.2f}%\n\n"
395
+
396
+ # Step 5: Summary of results
397
+ log += "## Summary of Results\n"
398
+ log += f"- Total events analyzed: {len(sample_filtered)}\n"
399
+ log += f"- Senescent cells (SA-β-Gal+): {senescent_percentage:.2f}%\n"
400
+ log += f"- Early apoptotic cells (Annexin V+/7-AAD-): {early_apoptotic_percentage:.2f}%\n"
401
+ log += f"- Late apoptotic cells (Annexin V+/7-AAD+): {late_apoptotic_percentage:.2f}%\n"
402
+ log += f"- Total apoptotic cells: {total_apoptotic_percentage:.2f}%\n"
403
+
404
+ # Save results to CSV file
405
+ results_file = os.path.splitext(os.path.basename(fcs_file_path))[0] + "_results.csv"
406
+ with open(results_file, "w") as f:
407
+ f.write("Cell Population,Percentage\n")
408
+ f.write(f"Senescent cells,{senescent_percentage:.2f}\n")
409
+ f.write(f"Early apoptotic cells,{early_apoptotic_percentage:.2f}\n")
410
+ f.write(f"Late apoptotic cells,{late_apoptotic_percentage:.2f}\n")
411
+ f.write(f"Total apoptotic cells,{total_apoptotic_percentage:.2f}\n")
412
+
413
+ log += f"\nResults saved to: {results_file}\n"
414
+
415
+ except Exception as e:
416
+ log += "\n## ERROR: An error occurred during analysis\n"
417
+ log += f"- Error message: {str(e)}\n"
418
+
419
+ return log
420
+
421
+
422
+ def detect_and_annotate_somatic_mutations(
423
+ tumor_bam, normal_bam, reference_genome, output_prefix, snpeff_database="GRCh38.105"
424
+ ):
425
+ """Detects and annotates somatic mutations in tumor samples compared to matched normal samples.
426
+
427
+ This function uses GATK Mutect2 for variant calling, GATK FilterMutectCalls for filtering,
428
+ and SnpEff for functional annotation of somatic mutations.
429
+
430
+ Parameters
431
+ ----------
432
+ tumor_bam : str
433
+ Path to the tumor sample BAM file
434
+ normal_bam : str
435
+ Path to the matched normal sample BAM file
436
+ reference_genome : str
437
+ Path to the reference genome FASTA file
438
+ output_prefix : str
439
+ Prefix for output files
440
+ snpeff_database : str, optional
441
+ SnpEff database to use for annotation (default: "GRCh38.105")
442
+
443
+ Returns
444
+ -------
445
+ str
446
+ A research log summarizing the steps performed and results obtained
447
+
448
+ """
449
+ import datetime
450
+ import os
451
+ import subprocess
452
+
453
+ # Initialize research log
454
+ log = "# Somatic Mutation Analysis Log\n"
455
+ log += f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
456
+ log += "## Input Files\n"
457
+ log += f"- Tumor BAM: {tumor_bam}\n"
458
+ log += f"- Normal BAM: {normal_bam}\n"
459
+ log += f"- Reference Genome: {reference_genome}\n\n"
460
+
461
+ # Step 1: Run Mutect2 for somatic variant calling
462
+ log += "## Step 1: Somatic Variant Calling with Mutect2\n"
463
+ raw_vcf = f"{output_prefix}.unfiltered.vcf"
464
+
465
+ mutect2_cmd = [
466
+ "gatk",
467
+ "Mutect2",
468
+ "-R",
469
+ reference_genome,
470
+ "-I",
471
+ tumor_bam,
472
+ "-I",
473
+ normal_bam,
474
+ "-normal",
475
+ os.path.basename(normal_bam).split(".")[0],
476
+ "-O",
477
+ raw_vcf,
478
+ ]
479
+
480
+ log += f"Running command: {' '.join(mutect2_cmd)}\n"
481
+ try:
482
+ subprocess.run(mutect2_cmd, check=True, capture_output=True)
483
+ log += f"Successfully generated raw VCF: {raw_vcf}\n\n"
484
+ except subprocess.CalledProcessError as e:
485
+ log += f"Error running Mutect2: {str(e)}\n"
486
+ return log
487
+
488
+ # Step 2: Filter somatic calls
489
+ log += "## Step 2: Filtering Somatic Variants\n"
490
+ filtered_vcf = f"{output_prefix}.filtered.vcf"
491
+
492
+ filter_cmd = [
493
+ "gatk",
494
+ "FilterMutectCalls",
495
+ "-R",
496
+ reference_genome,
497
+ "-V",
498
+ raw_vcf,
499
+ "-O",
500
+ filtered_vcf,
501
+ ]
502
+
503
+ log += f"Running command: {' '.join(filter_cmd)}\n"
504
+ try:
505
+ subprocess.run(filter_cmd, check=True, capture_output=True)
506
+ log += f"Successfully filtered variants: {filtered_vcf}\n\n"
507
+ except subprocess.CalledProcessError as e:
508
+ log += f"Error filtering variants: {str(e)}\n"
509
+ return log
510
+
511
+ # Step 3: Annotate variants with SnpEff
512
+ log += "## Step 3: Functional Annotation with SnpEff\n"
513
+ annotated_vcf = f"{output_prefix}.annotated.vcf"
514
+
515
+ snpeff_cmd = ["snpEff", "-v", snpeff_database, filtered_vcf, ">", annotated_vcf]
516
+
517
+ log += f"Running command: {' '.join(snpeff_cmd)}\n"
518
+ try:
519
+ # Using shell=True because of the redirection
520
+ subprocess.run(" ".join(snpeff_cmd), shell=True, check=True)
521
+ log += f"Successfully annotated variants: {annotated_vcf}\n\n"
522
+ except subprocess.CalledProcessError as e:
523
+ log += f"Error annotating variants: {str(e)}\n"
524
+ return log
525
+
526
+ # Step 4: Generate summary statistics
527
+ log += "## Step 4: Generating Summary Statistics\n"
528
+ summary_file = f"{output_prefix}_mutation_summary.txt"
529
+
530
+ # Count total variants
531
+ count_cmd = f"grep -v '^#' {annotated_vcf} | wc -l"
532
+ try:
533
+ total_variants = subprocess.check_output(count_cmd, shell=True).decode().strip()
534
+ log += f"Total somatic variants detected: {total_variants}\n"
535
+ except subprocess.CalledProcessError as e:
536
+ log += f"Error counting variants: {str(e)}\n"
537
+
538
+ # Count by variant type
539
+ log += "Variant types:\n"
540
+ for variant_type in ["SNP", "INS", "DEL"]:
541
+ count_type_cmd = f"grep -v '^#' {annotated_vcf} | grep '{variant_type}' | wc -l"
542
+ try:
543
+ type_count = subprocess.check_output(count_type_cmd, shell=True).decode().strip()
544
+ log += f"- {variant_type}: {type_count}\n"
545
+ except subprocess.CalledProcessError:
546
+ log += f"- {variant_type}: Error counting\n"
547
+
548
+ # Count high impact variants
549
+ high_impact_cmd = f"grep -v '^#' {annotated_vcf} | grep 'HIGH' | wc -l"
550
+ try:
551
+ high_impact = subprocess.check_output(high_impact_cmd, shell=True).decode().strip()
552
+ log += f"High impact variants: {high_impact}\n\n"
553
+ except subprocess.CalledProcessError:
554
+ log += "High impact variants: Error counting\n\n"
555
+
556
+ # Step 5: Save summary to file
557
+ with open(summary_file, "w") as f:
558
+ f.write("Somatic Mutation Analysis Summary\n")
559
+ f.write(f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
560
+ f.write(f"Total somatic variants: {total_variants}\n")
561
+ f.write(f"Output VCF file: {annotated_vcf}\n")
562
+
563
+ log += "## Results\n"
564
+ log += "Analysis complete. Results saved to:\n"
565
+ log += f"- Annotated VCF: {annotated_vcf}\n"
566
+ log += f"- Summary file: {summary_file}\n\n"
567
+
568
+ return log
569
+
570
+
571
+ def detect_and_characterize_structural_variations(
572
+ bam_file_path,
573
+ reference_genome_path,
574
+ output_dir,
575
+ cosmic_db_path=None,
576
+ clinvar_db_path=None,
577
+ ):
578
+ """Detects and characterizes structural variations (SVs) in genomic sequencing data.
579
+
580
+ This function uses LUMPY for SV detection followed by annotation with COSMIC and/or ClinVar
581
+ databases to identify and characterize various types of structural variations including
582
+ deletions, inversions, translocations, and duplications.
583
+
584
+ Parameters
585
+ ----------
586
+ bam_file_path : str
587
+ Path to the aligned sequencing data in BAM format
588
+ reference_genome_path : str
589
+ Path to the reference genome in FASTA format
590
+ output_dir : str
591
+ Directory where results will be saved
592
+ cosmic_db_path : str, optional
593
+ Path to the COSMIC database for cancer annotation
594
+ clinvar_db_path : str, optional
595
+ Path to the ClinVar database for clinical annotation
596
+
597
+ Returns
598
+ -------
599
+ str
600
+ A research log summarizing the steps performed and results obtained
601
+
602
+ """
603
+ import datetime
604
+ import os
605
+ import subprocess
606
+
607
+ # Create output directory if it doesn't exist
608
+ os.makedirs(output_dir, exist_ok=True)
609
+
610
+ # Initialize research log
611
+ log = []
612
+ log.append("## Structural Variation Detection and Characterization")
613
+ log.append(f"Started at: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
614
+ log.append(f"Input BAM file: {bam_file_path}")
615
+ log.append(f"Reference genome: {reference_genome_path}")
616
+ log.append(f"Output directory: {output_dir}")
617
+ log.append("\n")
618
+
619
+ # Step 1: Extract discordant read-pairs and split-reads for LUMPY
620
+ log.append("### Step 1: Extracting discordant read-pairs and split-reads")
621
+
622
+ discordant_bam = os.path.join(output_dir, "discordant.bam")
623
+ split_bam = os.path.join(output_dir, "split.bam")
624
+
625
+ try:
626
+ # Extract discordant read pairs
627
+ subprocess.run(
628
+ [
629
+ "samtools",
630
+ "view",
631
+ "-b",
632
+ "-F",
633
+ "1294",
634
+ "-o",
635
+ discordant_bam,
636
+ bam_file_path,
637
+ ],
638
+ check=True,
639
+ )
640
+
641
+ # Extract split reads
642
+ subprocess.run(
643
+ [
644
+ "samtools",
645
+ "view",
646
+ "-h",
647
+ bam_file_path,
648
+ "|",
649
+ "extractSplitReads_BwaMem",
650
+ "-i",
651
+ "stdin",
652
+ "|",
653
+ "samtools",
654
+ "view",
655
+ "-Sb",
656
+ "-",
657
+ ">",
658
+ split_bam,
659
+ ],
660
+ shell=True,
661
+ check=True,
662
+ )
663
+
664
+ log.append("Successfully extracted discordant read-pairs and split-reads")
665
+ except subprocess.CalledProcessError as e:
666
+ log.append(f"Error during read extraction: {e}")
667
+ return "\n".join(log)
668
+
669
+ # Step 2: Run LUMPY for SV detection
670
+ log.append("\n### Step 2: Running LUMPY for structural variation detection")
671
+
672
+ vcf_output = os.path.join(output_dir, "structural_variants.vcf")
673
+
674
+ try:
675
+ subprocess.run(
676
+ [
677
+ "lumpyexpress",
678
+ "-B",
679
+ bam_file_path,
680
+ "-S",
681
+ split_bam,
682
+ "-D",
683
+ discordant_bam,
684
+ "-o",
685
+ vcf_output,
686
+ ],
687
+ check=True,
688
+ )
689
+
690
+ log.append("LUMPY analysis completed successfully")
691
+ log.append(f"Raw SV calls saved to: {vcf_output}")
692
+ except subprocess.CalledProcessError as e:
693
+ log.append(f"Error during LUMPY execution: {e}")
694
+ return "\n".join(log)
695
+
696
+ # Step 3: Filter SVs by quality and size
697
+ log.append("\n### Step 3: Filtering structural variants by quality and size")
698
+
699
+ filtered_vcf = os.path.join(output_dir, "filtered_structural_variants.vcf")
700
+
701
+ try:
702
+ # Filter SVs with quality score >= 100 and size >= 100bp
703
+ subprocess.run(
704
+ [
705
+ "bcftools",
706
+ "filter",
707
+ "-i",
708
+ "QUAL>=100 && SVLEN>=100",
709
+ "-o",
710
+ filtered_vcf,
711
+ vcf_output,
712
+ ],
713
+ check=True,
714
+ )
715
+
716
+ # Count SVs by type
717
+ sv_counts = {}
718
+ sv_types = ["DEL", "DUP", "INV", "BND", "INS"]
719
+
720
+ for sv_type in sv_types:
721
+ result = subprocess.run(
722
+ ["grep", "-c", f"SVTYPE={sv_type}", filtered_vcf],
723
+ check=False,
724
+ capture_output=True,
725
+ text=True,
726
+ )
727
+
728
+ count = 0
729
+ if result.returncode == 0:
730
+ count = int(result.stdout.strip())
731
+ sv_counts[sv_type] = count
732
+
733
+ log.append("SV filtering completed")
734
+ log.append(f"Filtered SVs saved to: {filtered_vcf}")
735
+ log.append("\nSV counts by type:")
736
+ for sv_type, count in sv_counts.items():
737
+ log.append(f"- {sv_type}: {count}")
738
+ except subprocess.CalledProcessError as e:
739
+ log.append(f"Error during SV filtering: {e}")
740
+ return "\n".join(log)
741
+
742
+ # Step 4: Annotate SVs with COSMIC and ClinVar (if databases provided)
743
+ log.append("\n### Step 4: Annotating structural variants")
744
+
745
+ annotated_vcf = os.path.join(output_dir, "annotated_structural_variants.vcf")
746
+
747
+ try:
748
+ annotation_cmd = ["annotate_sv.py", "-i", filtered_vcf, "-o", annotated_vcf]
749
+
750
+ if cosmic_db_path:
751
+ annotation_cmd.extend(["-c", cosmic_db_path])
752
+ log.append(f"Using COSMIC database: {cosmic_db_path}")
753
+
754
+ if clinvar_db_path:
755
+ annotation_cmd.extend(["-v", clinvar_db_path])
756
+ log.append(f"Using ClinVar database: {clinvar_db_path}")
757
+
758
+ # This is a placeholder for the annotation command
759
+ # In a real implementation, you would use a tool like AnnotSV, VEP, or a custom script
760
+ log.append("Note: Annotation step is simulated in this implementation")
761
+ log.append("In a real scenario, tools like AnnotSV or VEP would be used")
762
+
763
+ # Instead of running the command, we'll create a simple annotated file
764
+ with open(filtered_vcf) as infile, open(annotated_vcf, "w") as outfile:
765
+ for line in infile:
766
+ if line.startswith("#"):
767
+ outfile.write(line)
768
+ else:
769
+ outfile.write(line)
770
+ # In a real implementation, annotation would be added here
771
+
772
+ log.append("SV annotation completed")
773
+ log.append(f"Annotated SVs saved to: {annotated_vcf}")
774
+ except Exception as e:
775
+ log.append(f"Error during SV annotation: {e}")
776
+ return "\n".join(log)
777
+
778
+ # Step 5: Generate summary report
779
+ log.append("\n### Step 5: Generating summary report")
780
+
781
+ summary_file = os.path.join(output_dir, "sv_summary_report.tsv")
782
+
783
+ try:
784
+ # Convert VCF to a tabular format for easier analysis
785
+ subprocess.run(
786
+ [
787
+ "bcftools",
788
+ "query",
789
+ "-f",
790
+ "%CHROM\t%POS\t%INFO/SVTYPE\t%INFO/SVLEN\t%QUAL\n",
791
+ "-o",
792
+ summary_file,
793
+ annotated_vcf,
794
+ ],
795
+ check=True,
796
+ )
797
+
798
+ log.append(f"Summary report generated: {summary_file}")
799
+ except subprocess.CalledProcessError as e:
800
+ log.append(f"Error generating summary report: {e}")
801
+ return "\n".join(log)
802
+
803
+ # Final summary
804
+ log.append("\n## Summary")
805
+ log.append(f"Total SVs detected: {sum(sv_counts.values())}")
806
+ log.append("SV types breakdown:")
807
+ for sv_type, count in sv_counts.items():
808
+ log.append(f"- {sv_type}: {count} ({count / sum(sv_counts.values()) * 100:.1f}%)")
809
+
810
+ log.append("\nFiles generated:")
811
+ log.append(f"- Raw SV calls: {vcf_output}")
812
+ log.append(f"- Filtered SVs: {filtered_vcf}")
813
+ log.append(f"- Annotated SVs: {annotated_vcf}")
814
+ log.append(f"- Summary report: {summary_file}")
815
+
816
+ log.append(f"\nAnalysis completed at: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
817
+
818
+ return "\n".join(log)
819
+
820
+
821
+ def perform_gene_expression_nmf_analysis(
822
+ expression_data_path,
823
+ n_components=10,
824
+ normalize=True,
825
+ output_dir="nmf_results",
826
+ random_state=42,
827
+ ):
828
+ """Performs Non-negative Matrix Factorization (NMF) on gene expression data to extract
829
+ metagenes and their associated sample weights for tumor subtype identification.
830
+
831
+ Parameters
832
+ ----------
833
+ expression_data_path : str
834
+ Path to a CSV or TSV file containing gene expression data with genes as rows and samples as columns.
835
+ Values should be non-negative (e.g., normalized counts or expression values).
836
+ n_components : int, default=10
837
+ Number of metagenes (components) to extract.
838
+ normalize : bool, default=True
839
+ Whether to normalize the expression data before applying NMF.
840
+ output_dir : str, default="nmf_results"
841
+ Directory to save the output files.
842
+ random_state : int, default=42
843
+ Random seed for reproducibility.
844
+
845
+ Returns
846
+ -------
847
+ str
848
+ Research log summarizing the analysis steps and results.
849
+
850
+ """
851
+ import os
852
+ from datetime import datetime
853
+
854
+ import numpy as np
855
+ import pandas as pd
856
+ from sklearn.decomposition import NMF
857
+
858
+ # Start research log
859
+ log = []
860
+ log.append(f"NMF GENE EXPRESSION ANALYSIS - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
861
+ log.append(f"Parameters: n_components={n_components}, normalize={normalize}")
862
+
863
+ # Load expression data from file
864
+ try:
865
+ if expression_data_path.endswith(".csv"):
866
+ expression_data = pd.read_csv(expression_data_path, index_col=0)
867
+ elif expression_data_path.endswith((".tsv", ".txt")):
868
+ expression_data = pd.read_csv(expression_data_path, sep="\t", index_col=0)
869
+ else:
870
+ log.append("ERROR: Unsupported file format. Please provide a CSV or TSV file.")
871
+ return "\n".join(log)
872
+
873
+ log.append(f"Successfully loaded expression data from {expression_data_path}")
874
+ except Exception as e:
875
+ log.append(f"ERROR: Failed to load expression data from {expression_data_path}. Error: {str(e)}")
876
+ return "\n".join(log)
877
+
878
+ # Extract gene and sample information
879
+ genes = expression_data.index.tolist()
880
+ samples = expression_data.columns.tolist()
881
+ X = expression_data.values
882
+ log.append(f"Input data: {len(genes)} genes × {len(samples)} samples")
883
+
884
+ # Check for non-negative values
885
+ if np.any(X < 0):
886
+ log.append("WARNING: Negative values found in expression data. Converting to absolute values.")
887
+ X = np.abs(X)
888
+
889
+ # Normalize data if requested
890
+ if normalize:
891
+ log.append("Normalizing expression data...")
892
+ X = X / np.sum(X, axis=0, keepdims=True) * 1000 # TPM-like normalization
893
+
894
+ # Create output directory if it doesn't exist
895
+ if not os.path.exists(output_dir):
896
+ os.makedirs(output_dir)
897
+ log.append(f"Created output directory: {output_dir}")
898
+
899
+ # Apply NMF
900
+ log.append(f"Applying NMF to extract {n_components} metagenes...")
901
+ model = NMF(
902
+ n_components=n_components,
903
+ init="random",
904
+ random_state=random_state,
905
+ max_iter=1000,
906
+ )
907
+
908
+ try:
909
+ # W: metagenes (genes × components)
910
+ # H: sample weights (components × samples)
911
+ W = model.fit_transform(X)
912
+ H = model.components_
913
+
914
+ log.append(f"NMF completed successfully in {model.n_iter_} iterations")
915
+ log.append(f"Explained variance: {model.reconstruction_err_:.4f}")
916
+
917
+ # Save metagenes (W matrix)
918
+ metagenes_file = os.path.join(output_dir, "metagenes.csv")
919
+ pd.DataFrame(W, index=genes, columns=[f"Metagene_{i + 1}" for i in range(n_components)]).to_csv(metagenes_file)
920
+ log.append(f"Saved metagenes to {metagenes_file}")
921
+
922
+ # Save sample weights (H matrix)
923
+ weights_file = os.path.join(output_dir, "sample_weights.csv")
924
+ pd.DataFrame(H, index=[f"Metagene_{i + 1}" for i in range(n_components)], columns=samples).to_csv(weights_file)
925
+ log.append(f"Saved sample weights to {weights_file}")
926
+
927
+ top_genes_file = os.path.join(output_dir, "top_genes_per_metagene.csv")
928
+ top_genes = {}
929
+ for i in range(n_components):
930
+ # Get indices of top 20 genes for this metagene
931
+ top_indices = np.argsort(W[:, i])[::-1][:20]
932
+ top_genes[f"Metagene_{i + 1}"] = [genes[idx] for idx in top_indices]
933
+
934
+ pd.DataFrame(top_genes).to_csv(top_genes_file)
935
+ log.append(f"Saved top genes per metagene to {top_genes_file}")
936
+
937
+ except Exception as e:
938
+ log.append(f"ERROR: NMF failed with error: {str(e)}")
939
+ return "\n".join(log)
940
+
941
+ log.append("\nSUMMARY:")
942
+ log.append(f"- Successfully extracted {n_components} metagenes from {X.shape[0]} genes across {X.shape[1]} samples")
943
+ log.append(f"- Results saved to: {output_dir}")
944
+ log.append("- Files generated: metagenes.csv, sample_weights.csv, top_genes_per_metagene.csv")
945
+ log.append("\nNEXT STEPS:")
946
+ log.append("- Analyze the metagenes to identify biological pathways")
947
+ log.append("- Cluster samples based on metagene weights to identify potential tumor subtypes")
948
+ log.append("- Correlate subtypes with clinical outcomes if available")
949
+
950
+ return "\n".join(log)
951
+
952
+
953
+ def analyze_copy_number_purity_ploidy_and_focal_events(
954
+ tumor_bam,
955
+ reference_genome,
956
+ normal_bam=None,
957
+ output_dir="cn_analysis_results",
958
+ targets_bed=None,
959
+ antitargets_bed=None,
960
+ gene_bed=None,
961
+ focal_genes=None,
962
+ log2_amp_threshold=1.0,
963
+ log2_del_threshold=-1.0,
964
+ ):
965
+ """CNVkit-based copy number workflow: CNV segmentation, purity/ploidy & HRD approximation, focal events.
966
+
967
+ This function orchestrates a CNVkit-based copy number analysis workflow for tumor samples.
968
+ It performs CNV segmentation, derives approximate purity & ploidy metrics, calculates a simplified
969
+ HRD-style summary, and detects focal amplifications/deletions in key oncogenes / tumor suppressors.
970
+
971
+ Parameters
972
+ ----------
973
+ tumor_bam : str
974
+ Path to tumor BAM (indexed)
975
+ reference_genome : str
976
+ Path to reference FASTA (with index files present)
977
+ normal_bam : str, optional
978
+ Matched normal BAM (recommended for allele-specific & purity inference)
979
+ output_dir : str, default "cn_analysis_results"
980
+ Directory for all outputs
981
+ targets_bed : str, optional
982
+ BED of target regions (e.g. exome / panel) for CNVkit / gCNV
983
+ antitargets_bed : str, optional
984
+ BED of antitarget regions for CNVkit (if panel / exome)
985
+ gene_bed : str, optional
986
+ BED file with gene coordinates (chrom, start, end, gene). Used for focal event annotation
987
+ focal_genes : list[str], optional
988
+ List of genes to highlight for focal amplification/deletion; defaults to ["MYC","ERBB2","CDKN2A"]
989
+ log2_amp_threshold : float, default 1.0
990
+ Log2 ratio threshold to call focal amplification (~ >2x copy relative to baseline)
991
+ log2_del_threshold : float, default -1.0
992
+ Log2 ratio threshold to call focal deep deletion (~ homozygous loss)
993
+
994
+ Returns
995
+ -------
996
+ str
997
+ Research log summarizing steps, tool executions, and findings.
998
+ """
999
+ import datetime
1000
+ import math
1001
+ import os
1002
+ import shutil
1003
+ import statistics
1004
+ import subprocess
1005
+
1006
+ import pandas as pd
1007
+
1008
+ log = []
1009
+ ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1010
+ log.append(f"CNVKIT COPY NUMBER ANALYSIS WORKFLOW - {ts}")
1011
+ log.append("=" * 80)
1012
+ log.append(f"Tumor BAM: {tumor_bam}")
1013
+ if normal_bam:
1014
+ log.append(f"Normal BAM: {normal_bam}")
1015
+ log.append(f"Reference genome: {reference_genome}")
1016
+ log.append(f"Output directory: {output_dir}\n")
1017
+
1018
+ os.makedirs(output_dir, exist_ok=True)
1019
+ sample_name = os.path.splitext(os.path.basename(tumor_bam))[0]
1020
+
1021
+ if focal_genes is None:
1022
+ focal_genes = ["MYC", "ERBB2", "CDKN2A"]
1023
+ # Single informative log line (was duplicated previously)
1024
+ log.append(f"Focal genes for analysis: {', '.join(focal_genes)}")
1025
+
1026
+ # ----------------------------------------------------------------------------------
1027
+ # STEP 1: CNV SEGMENTATION with CNVkit
1028
+ # ----------------------------------------------------------------------------------
1029
+ log.append("STEP 1: CNV Segmentation with CNVkit")
1030
+ cnvkit_path = shutil.which("cnvkit.py") or shutil.which("cnvkit")
1031
+ use_conda_env = False
1032
+
1033
+ # Check if CNVkit is available in biomni_e1 environment
1034
+ if not cnvkit_path:
1035
+ try:
1036
+ result = subprocess.run(
1037
+ ["conda", "run", "-n", "biomni_e1", "which", "cnvkit.py"], capture_output=True, text=True, check=True
1038
+ )
1039
+ if result.returncode == 0 and result.stdout.strip():
1040
+ cnvkit_path = "cnvkit.py" # Will use via conda run
1041
+ use_conda_env = True
1042
+ log.append("- CNVkit found in biomni_e1 environment")
1043
+ except (subprocess.CalledProcessError, FileNotFoundError):
1044
+ pass
1045
+
1046
+ # Fallback to bio_env_py310
1047
+ if not cnvkit_path:
1048
+ try:
1049
+ result = subprocess.run(
1050
+ ["conda", "run", "-n", "bio_env_py310", "which", "cnvkit.py"],
1051
+ capture_output=True,
1052
+ text=True,
1053
+ check=True,
1054
+ )
1055
+ if result.returncode == 0 and result.stdout.strip():
1056
+ cnvkit_path = "cnvkit.py" # Will use via conda run
1057
+ use_conda_env = True
1058
+ log.append("- CNVkit found in bio_env_py310 environment")
1059
+ except (subprocess.CalledProcessError, FileNotFoundError):
1060
+ pass
1061
+
1062
+ if not cnvkit_path:
1063
+ log.append("- CNVkit not detected in current PATH, biomni_e1, or bio_env_py310 environment")
1064
+ log.append("- Install manually (e.g., pip install cnvkit==0.9.11) or run setup.sh")
1065
+ log.append("- Workflow will exit without CNVkit")
1066
+ return "\n".join(log)
1067
+
1068
+ cnvkit_cns = None
1069
+ log.append(f"- CNVkit detected at: {cnvkit_path}")
1070
+
1071
+ # Determine environment prefix for CNVkit commands
1072
+ env_prefix = ["conda", "run", "-n", "biomni_e1"] if use_conda_env else []
1073
+
1074
+ # CNVkit batch command
1075
+ batch_cmd = env_prefix + [cnvkit_path, "batch", tumor_bam, "-d", output_dir, "-f", reference_genome]
1076
+ if normal_bam:
1077
+ batch_cmd.extend(["-n", normal_bam])
1078
+ if targets_bed:
1079
+ batch_cmd.extend(["-t", targets_bed])
1080
+ if antitargets_bed:
1081
+ batch_cmd.extend(["-a", antitargets_bed])
1082
+ batch_cmd.extend(["--scatter", "--diagram"])
1083
+
1084
+ log.append(f"- Running CNVkit batch command: {' '.join(batch_cmd)}")
1085
+ try:
1086
+ subprocess.run(batch_cmd, check=True, capture_output=True)
1087
+ log.append("- CNVkit batch completed successfully")
1088
+ except subprocess.CalledProcessError as e:
1089
+ log.append(f"- WARNING: CNVkit batch failed: {e}. Proceeding with limited downstream analyses")
1090
+
1091
+ # Check for CNVkit output and run call command
1092
+ cnvkit_cns = os.path.join(output_dir, f"{sample_name}.cns")
1093
+ if os.path.exists(cnvkit_cns):
1094
+ call_cmd = env_prefix + [
1095
+ cnvkit_path,
1096
+ "call",
1097
+ cnvkit_cns,
1098
+ "-o",
1099
+ os.path.join(output_dir, f"{sample_name}.call.cns"),
1100
+ "-m",
1101
+ "clonal",
1102
+ ]
1103
+ log.append(f"- Calling absolute CN: {' '.join(call_cmd)}")
1104
+ try:
1105
+ subprocess.run(call_cmd, check=True, capture_output=True)
1106
+ cnvkit_cns = os.path.join(output_dir, f"{sample_name}.call.cns")
1107
+ log.append("- CNVkit call succeeded")
1108
+ except subprocess.CalledProcessError as e:
1109
+ log.append(f"- WARNING: CNVkit call failed: {e}")
1110
+ else:
1111
+ log.append("- WARNING: Expected CNVkit .cns file not found")
1112
+
1113
+ # ----------------------------------------------------------------------------------
1114
+ # STEP 2: Purity & ploidy approximation (based on CNVkit outputs)
1115
+ # ----------------------------------------------------------------------------------
1116
+ log.append("\nSTEP 2: Purity & Ploidy Approximation")
1117
+ purity_est = None
1118
+ ploidy_est = None
1119
+ seg_source = None
1120
+ seg_df = None
1121
+ try:
1122
+ if cnvkit_cns and os.path.exists(cnvkit_cns):
1123
+ seg_source = cnvkit_cns
1124
+ seg_df = pd.read_csv(cnvkit_cns, sep="\t")
1125
+ log.append(f"- Using CNVkit segments from: {seg_source}")
1126
+
1127
+ # Harmonize columns
1128
+ cols = [c.lower() for c in seg_df.columns]
1129
+ seg_df.columns = cols
1130
+
1131
+ # Obtain log2 column
1132
+ log2_col = None
1133
+ for candidate in ["log2", "log2ratio", "cnlr", "logr"]:
1134
+ if candidate in cols:
1135
+ log2_col = candidate
1136
+ break
1137
+ for candidate in ["end", "chromend", "stop"]:
1138
+ if candidate in cols:
1139
+ end_col = candidate
1140
+ break
1141
+ for candidate in ["start", "chromstart", "begin"]:
1142
+ if candidate in cols:
1143
+ start_col = candidate
1144
+ break
1145
+
1146
+ if log2_col and start_col and end_col:
1147
+ seg_df["seg_length"] = seg_df[end_col] - seg_df[start_col]
1148
+ # Weighted average absolute copy ratio -> approximate ploidy baseline
1149
+ # Convert log2 ratio to absolute copy ratio (assuming diploid baseline = 2)
1150
+ seg_df["abs_cn"] = 2 * (2 ** seg_df[log2_col])
1151
+ weighted_mean_cn = (seg_df["abs_cn"] * seg_df["seg_length"]).sum() / seg_df["seg_length"].sum()
1152
+ ploidy_est = weighted_mean_cn
1153
+ # Approximate purity: higher variance in log2 ratios suggests purity; naive formula
1154
+ mad_log2 = statistics.median([abs(x) for x in seg_df[log2_col] if not math.isnan(x)])
1155
+ purity_est = min(1.0, max(0.2, 1 - mad_log2 / 1.5)) # heuristic clamp
1156
+ log.append(f"- Estimated ploidy (weighted mean CN): {ploidy_est:.2f}")
1157
+ log.append(f"- Estimated purity (heuristic from segmentation dispersion): {purity_est:.2f}")
1158
+ else:
1159
+ log.append("- WARNING: Could not identify necessary columns for purity/ploidy estimation")
1160
+ else:
1161
+ log.append("- No CNVkit segmentation available for purity/ploidy estimation")
1162
+ except Exception as e:
1163
+ log.append(f"- ERROR: Purity/ploidy estimation failed: {e}")
1164
+
1165
+ # ----------------------------------------------------------------------------------
1166
+ # STEP 3: HRD-like summary (simplified proxy)
1167
+ # ----------------------------------------------------------------------------------
1168
+ log.append("\nSTEP 3: HRD Approximation (Simplified)")
1169
+ hrd_metrics = {}
1170
+ try:
1171
+ if seg_df is not None and "seg_length" in seg_df:
1172
+ # Count large-scale transitions (LST proxy: adjacent segments >10 Mb with log2 change >0.2)
1173
+ seg_df_sorted = seg_df.sort_values(
1174
+ by=[c for c in seg_df.columns if c.startswith("chrom") or c == "chrom" or c == "chromosome"][0]
1175
+ if any(c.startswith("chrom") for c in seg_df.columns)
1176
+ else seg_df.columns[0]
1177
+ )
1178
+ lst_count = 0
1179
+ prev = None
1180
+ for _, row in seg_df_sorted.iterrows():
1181
+ if prev is not None:
1182
+ if row["seg_length"] >= 10_000_000 and prev["seg_length"] >= 10_000_000:
1183
+ if log2_col and abs(row[log2_col] - prev[log2_col]) > 0.2:
1184
+ lst_count += 1
1185
+ prev = row
1186
+ hrd_metrics["LST_like"] = lst_count
1187
+ # HRD-LOH proxy: segments with log2 < -0.3 and length >15 Mb
1188
+ if log2_col:
1189
+ hrd_loh = ((seg_df[log2_col] < -0.3) & (seg_df["seg_length"] > 15_000_000)).sum()
1190
+ hrd_metrics["HRD_LOH_like"] = int(hrd_loh)
1191
+ # Telomeric AI proxy omitted (needs allele-specific data)
1192
+ hrd_score = sum(hrd_metrics.values())
1193
+ hrd_metrics["Simplified_HRD_score"] = hrd_score
1194
+ log.append(f"- LST-like events: {hrd_metrics['LST_like']}")
1195
+ log.append(f"- HRD-LOH-like events: {hrd_metrics['HRD_LOH_like']}")
1196
+ log.append(f"- Simplified composite HRD score: {hrd_metrics['Simplified_HRD_score']}")
1197
+ log.append("- NOTE: For clinical/research use, apply scarHRD or HRDetect with allele-specific data")
1198
+ else:
1199
+ log.append("- Segmentation not available; HRD approximation skipped")
1200
+ except Exception as e:
1201
+ log.append(f"- ERROR: HRD approximation failed: {e}")
1202
+
1203
+ # ----------------------------------------------------------------------------------
1204
+ # STEP 4: Focal amplifications/deletions in key genes
1205
+ # ----------------------------------------------------------------------------------
1206
+ log.append("\nSTEP 4: Focal Amplifications / Deletions in Key Genes")
1207
+ focal_events = []
1208
+ try:
1209
+ if seg_df is not None and gene_bed and os.path.exists(gene_bed):
1210
+ genes_df = pd.read_csv(gene_bed, sep="\t", header=None, names=["chrom", "start", "end", "gene"])
1211
+
1212
+ # Normalize chromosome naming
1213
+ def norm_chr(x):
1214
+ return x.replace("chr", "") if isinstance(x, str) else x
1215
+
1216
+ seg_df["chr_norm"] = (
1217
+ seg_df[[c for c in seg_df.columns if c in ["chrom", "chromosome", "chr"]][0]].astype(str).map(norm_chr)
1218
+ )
1219
+ genes_df["chr_norm"] = genes_df["chrom"].astype(str).map(norm_chr)
1220
+ for gene in focal_genes:
1221
+ region = genes_df[genes_df["gene"] == gene]
1222
+ if region.empty:
1223
+ continue
1224
+ r = region.iloc[0]
1225
+ # Ensure we work on a copy to avoid SettingWithCopyWarning
1226
+ overlaps = seg_df[
1227
+ (seg_df["chr_norm"] == r["chr_norm"])
1228
+ & (seg_df[start_col] <= r["end"])
1229
+ & (seg_df[end_col] >= r["start"])
1230
+ ].copy()
1231
+ if not overlaps.empty and log2_col:
1232
+ # Choose segment with largest overlap length
1233
+ r_end = r["end"]
1234
+ r_start = r["start"]
1235
+ overlaps["ov_len"] = overlaps.apply(
1236
+ lambda row, r_end=r_end, r_start=r_start: min(row[end_col], r_end)
1237
+ - max(row[start_col], r_start),
1238
+ axis=1,
1239
+ )
1240
+ best = overlaps.sort_values("ov_len", ascending=False).iloc[0]
1241
+ l2 = best[log2_col]
1242
+ status = None
1243
+ if l2 >= log2_amp_threshold:
1244
+ status = "AMPLIFICATION"
1245
+ elif l2 <= log2_del_threshold:
1246
+ status = "DELETION"
1247
+ if status:
1248
+ focal_events.append((gene, status, l2))
1249
+ if focal_events:
1250
+ for gene, status, l2 in focal_events:
1251
+ log.append(f"- {gene}: {status} (log2={l2:.2f})")
1252
+ else:
1253
+ log.append("- No focal events meeting thresholds in target genes")
1254
+ else:
1255
+ if not gene_bed:
1256
+ log.append("- Skipped: gene_bed not provided")
1257
+ else:
1258
+ log.append("- Skipped: gene_bed file not found or segmentation unavailable")
1259
+ except Exception as e:
1260
+ log.append(f"- ERROR: Focal event detection failed: {e}")
1261
+
1262
+ # ----------------------------------------------------------------------------------
1263
+ # STEP 5: Summaries & file outputs
1264
+ # ----------------------------------------------------------------------------------
1265
+ log.append("\nSTEP 5: Summary & Output Files")
1266
+ summary = {
1267
+ "purity_estimate": purity_est,
1268
+ "ploidy_estimate": ploidy_est,
1269
+ }
1270
+ summary.update(hrd_metrics)
1271
+ summary_file = os.path.join(output_dir, f"{sample_name}_cn_summary.tsv")
1272
+ try:
1273
+ pd.DataFrame([summary]).to_csv(summary_file, sep="\t", index=False)
1274
+ log.append(f"- Saved summary metrics: {summary_file}")
1275
+ except Exception as e:
1276
+ log.append(f"- WARNING: Could not save summary file: {e}")
1277
+
1278
+ if focal_events:
1279
+ focal_file = os.path.join(output_dir, f"{sample_name}_focal_events.tsv")
1280
+ try:
1281
+ pd.DataFrame(focal_events, columns=["Gene", "Event", "Log2"]).to_csv(focal_file, sep="\t", index=False)
1282
+ log.append(f"- Saved focal events: {focal_file}")
1283
+ except Exception as e:
1284
+ log.append(f"- WARNING: Could not save focal events file: {e}")
1285
+
1286
+ log.append("\nNEXT STEPS:")
1287
+ log.append("- For improved purity/ploidy estimation, consider ABSOLUTE, FACETS, PureCN, or Sequenza")
1288
+ log.append("- For validated HRD scoring, use scarHRD or HRDetect with allele-specific data")
1289
+ log.append("- Integrate CN events with expression data to assess driver gene activation/dosage")
1290
+ log.append("- Visualize CNV profiles using CNVkit's built-in plotting functions")
1291
+
1292
+ return "\n".join(log)
Biomanus_upload/biomni/tool/cell_biology.py ADDED
@@ -0,0 +1,742 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def quantify_cell_cycle_phases_from_microscopy(image_paths, output_dir="./results"):
2
+ """Quantify the percentage of cells in each cell cycle phase using Calcofluor white stained microscopy images.
3
+
4
+ This function processes microscopy images where cell walls/septa are stained with Calcofluor white,
5
+ segments individual cells, extracts features, and classifies each cell into G1, S, or G2/M phase.
6
+
7
+ Parameters
8
+ ----------
9
+ image_paths : list of str
10
+ List of file paths to microscopy images of cells stained with Calcofluor white
11
+ output_dir : str, optional
12
+ Directory to save results (default: './results')
13
+
14
+ Returns
15
+ -------
16
+ str
17
+ Research log summarizing the analysis process and results
18
+
19
+ """
20
+ import os
21
+
22
+ import numpy as np
23
+ import pandas as pd
24
+ from skimage import filters, io, measure, morphology
25
+
26
+ # Create output directory if it doesn't exist
27
+ os.makedirs(output_dir, exist_ok=True)
28
+
29
+ # Initialize log
30
+ log = "# Cell Cycle Phase Quantification Analysis Log\n\n"
31
+ log += f"Processing {len(image_paths)} microscopy images with Calcofluor white staining.\n\n"
32
+
33
+ # Initialize data structures to store results
34
+ all_cells = []
35
+ cell_features = []
36
+
37
+ # Step 1: Process images and segment cells
38
+ log += "## Image Processing and Cell Segmentation\n\n"
39
+
40
+ for i, img_path in enumerate(image_paths):
41
+ log += f"Processing image {i + 1}/{len(image_paths)}: {os.path.basename(img_path)}\n"
42
+
43
+ # Load image
44
+ img = io.imread(img_path)
45
+
46
+ # Convert to grayscale if needed
47
+ if len(img.shape) > 2:
48
+ img = np.mean(img, axis=2).astype(np.uint8)
49
+
50
+ # Preprocessing
51
+ filtered_img = filters.gaussian(img, sigma=1.0)
52
+
53
+ # Thresholding to identify cell walls
54
+ threshold = filters.threshold_otsu(filtered_img)
55
+ binary = filtered_img > threshold
56
+
57
+ # Clean up binary image
58
+ binary = morphology.remove_small_objects(binary, min_size=30)
59
+ binary = morphology.binary_closing(binary)
60
+
61
+ # Segment cells
62
+ labeled_cells = measure.label(binary)
63
+ cell_props = measure.regionprops(labeled_cells, intensity_image=img)
64
+
65
+ log += f" - Found {len(cell_props)} cells\n"
66
+ all_cells.extend(cell_props)
67
+
68
+ # Extract features for each cell
69
+ for cell in cell_props:
70
+ # Basic morphological features
71
+ area = cell.area
72
+ perimeter = cell.perimeter
73
+ eccentricity = cell.eccentricity
74
+ mean_intensity = cell.mean_intensity
75
+
76
+ # Cell shape features
77
+ circularity = (4 * np.pi * area) / (perimeter**2) if perimeter > 0 else 0
78
+
79
+ # Check for septation (indicative of S or G2/M phase)
80
+ # Calcofluor white stains the septum more intensely
81
+ has_septum = False
82
+ if area > 50: # Only check larger cells
83
+ intensity_profile = cell.intensity_image[cell.intensity_image > 0]
84
+ if intensity_profile.size > 0:
85
+ intensity_std = np.std(intensity_profile)
86
+ intensity_max = np.max(intensity_profile)
87
+ # High standard deviation and max intensity suggests septum presence
88
+ has_septum = intensity_std > 0.2 * np.mean(intensity_profile) and intensity_max > 1.5 * np.mean(
89
+ intensity_profile
90
+ )
91
+
92
+ # Store features
93
+ cell_features.append(
94
+ [
95
+ area,
96
+ perimeter,
97
+ eccentricity,
98
+ mean_intensity,
99
+ circularity,
100
+ has_septum,
101
+ i, # include image index
102
+ ]
103
+ )
104
+
105
+ log += f"\nTotal cells detected across all images: {len(all_cells)}\n\n"
106
+
107
+ # Step 2: Feature engineering and classification
108
+ log += "## Cell Cycle Phase Classification\n\n"
109
+
110
+ # Convert to numpy array for easier processing
111
+ X = np.array(cell_features)
112
+
113
+ # Simple rule-based classification
114
+ # This is a simplified approach - in practice, you would train a classifier on labeled data
115
+ # G1: Smaller cells, no septum
116
+ # S: Medium-sized cells, early septum formation
117
+ # G2/M: Larger cells, clear septum, often elongated
118
+
119
+ # Define classification rules based on features
120
+ phases = []
121
+ for i in range(X.shape[0]):
122
+ area = X[i, 0]
123
+ perimeter = X[i, 1]
124
+ eccentricity = X[i, 2]
125
+ has_septum = X[i, 5]
126
+
127
+ if has_septum and area > np.median(X[:, 0]) * 1.2:
128
+ # Larger cells with septum are likely in G2/M
129
+ phases.append("G2/M")
130
+ elif has_septum or (area > np.median(X[:, 0]) and eccentricity > 0.5):
131
+ # Cells with septum or larger elongated cells are likely in S
132
+ phases.append("S")
133
+ else:
134
+ # Smaller cells without septum are likely in G1
135
+ phases.append("G1")
136
+
137
+ # Calculate percentages
138
+ unique_phases, counts = np.unique(phases, return_counts=True)
139
+ percentages = (counts / len(phases)) * 100
140
+
141
+ # Create results table
142
+ results_df = pd.DataFrame({"Phase": unique_phases, "Count": counts, "Percentage": percentages})
143
+
144
+ # Save results
145
+ results_path = os.path.join(output_dir, "cell_cycle_phases.csv")
146
+ results_df.to_csv(results_path, index=False)
147
+
148
+ # Log results
149
+ log += "### Cell Cycle Phase Distribution\n\n"
150
+ for phase, count, percentage in zip(unique_phases, counts, percentages, strict=False):
151
+ log += f"- {phase}: {count} cells ({percentage:.2f}%)\n"
152
+
153
+ log += f"\nResults saved to: {results_path}\n"
154
+ log += "\nNote: This analysis uses simplified morphological features for classification. "
155
+ log += (
156
+ "For more accurate results, a supervised machine learning approach with labeled training data is recommended.\n"
157
+ )
158
+
159
+ return log
160
+
161
+
162
+ def quantify_and_cluster_cell_motility(image_sequence_path, output_dir="./results", num_clusters=3):
163
+ """Quantify cell motility features from time-lapse microscopy images and cluster cells based on motility patterns.
164
+
165
+ Parameters
166
+ ----------
167
+ image_sequence_path : str
168
+ Path to directory containing time-lapse microscopy images in sequential order
169
+ output_dir : str
170
+ Directory to save output files (default: "./results")
171
+ num_clusters : int
172
+ Number of motility pattern clusters to identify (default: 3)
173
+
174
+ Returns
175
+ -------
176
+ str
177
+ Research log summarizing the analysis process and results
178
+
179
+ """
180
+ import os
181
+
182
+ import cv2
183
+ import numpy as np
184
+ import pandas as pd
185
+ from sklearn.cluster import KMeans
186
+
187
+ # Create output directory if it doesn't exist
188
+ os.makedirs(output_dir, exist_ok=True)
189
+
190
+ # Step 1: Load image sequence
191
+ image_files = sorted(
192
+ [f for f in os.listdir(image_sequence_path) if f.endswith((".tif", ".tiff", ".png", ".jpg", ".jpeg"))]
193
+ )
194
+
195
+ if len(image_files) < 2:
196
+ return "Error: Insufficient images found. At least 2 time points are required."
197
+
198
+ # Step 2: Initialize cell tracking
199
+ first_image = cv2.imread(os.path.join(image_sequence_path, image_files[0]), cv2.IMREAD_GRAYSCALE)
200
+
201
+ # Simple cell detection using thresholding and contour detection
202
+ _, binary = cv2.threshold(first_image, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
203
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
204
+
205
+ # Extract cell centroids from first frame
206
+ cells = []
207
+ for i, contour in enumerate(contours):
208
+ if cv2.contourArea(contour) > 50: # Filter small noise
209
+ M = cv2.moments(contour)
210
+ if M["m00"] != 0:
211
+ cx = int(M["m10"] / M["m00"])
212
+ cy = int(M["m01"] / M["m00"])
213
+ cells.append({"id": i, "positions": [(cx, cy)], "frame_indices": [0]})
214
+
215
+ # Step 3: Track cells across frames
216
+ for frame_idx, img_file in enumerate(image_files[1:], 1):
217
+ img = cv2.imread(os.path.join(image_sequence_path, img_file), cv2.IMREAD_GRAYSCALE)
218
+ _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
219
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
220
+
221
+ # Get current frame centroids
222
+ current_centroids = []
223
+ for contour in contours:
224
+ if cv2.contourArea(contour) > 50:
225
+ M = cv2.moments(contour)
226
+ if M["m00"] != 0:
227
+ cx = int(M["m10"] / M["m00"])
228
+ cy = int(M["m01"] / M["m00"])
229
+ current_centroids.append((cx, cy))
230
+
231
+ # Match cells with nearest centroids in current frame
232
+ for cell in cells:
233
+ if len(cell["positions"]) == frame_idx: # Only track cells found in previous frame
234
+ prev_pos = cell["positions"][-1]
235
+
236
+ if current_centroids:
237
+ # Calculate distances to all current centroids
238
+ distances = [
239
+ np.sqrt((prev_pos[0] - c[0]) ** 2 + (prev_pos[1] - c[1]) ** 2) for c in current_centroids
240
+ ]
241
+ min_idx = np.argmin(distances)
242
+
243
+ # Only match if distance is below threshold (prevents incorrect matching)
244
+ if distances[min_idx] < 50: # Threshold distance in pixels
245
+ cell["positions"].append(current_centroids[min_idx])
246
+ cell["frame_indices"].append(frame_idx)
247
+ current_centroids.pop(min_idx) # Remove matched centroid
248
+
249
+ # Step 4: Calculate motility features for each cell
250
+ cell_features = []
251
+
252
+ for cell in cells:
253
+ # Only analyze cells tracked for at least 3 frames
254
+ if len(cell["positions"]) < 3:
255
+ continue
256
+
257
+ # Calculate displacements between consecutive positions
258
+ displacements = []
259
+ for i in range(1, len(cell["positions"])):
260
+ prev_pos = cell["positions"][i - 1]
261
+ curr_pos = cell["positions"][i]
262
+ displacement = np.sqrt((curr_pos[0] - prev_pos[0]) ** 2 + (curr_pos[1] - prev_pos[1]) ** 2)
263
+ displacements.append(displacement)
264
+
265
+ # Calculate speed (pixels per frame)
266
+ avg_speed = np.mean(displacements)
267
+
268
+ # Calculate directionality (displacement from start to end / total path length)
269
+ start_pos = cell["positions"][0]
270
+ end_pos = cell["positions"][-1]
271
+ net_displacement = np.sqrt((end_pos[0] - start_pos[0]) ** 2 + (end_pos[1] - start_pos[1]) ** 2)
272
+ total_path_length = np.sum(displacements)
273
+ directionality = net_displacement / total_path_length if total_path_length > 0 else 0
274
+
275
+ # Calculate mean squared displacement
276
+ msd = np.mean(
277
+ [
278
+ np.sqrt((cell["positions"][i][0] - start_pos[0]) ** 2 + (cell["positions"][i][1] - start_pos[1]) ** 2)
279
+ for i in range(1, len(cell["positions"]))
280
+ ]
281
+ )
282
+
283
+ # Calculate track duration
284
+ duration = cell["frame_indices"][-1] - cell["frame_indices"][0]
285
+
286
+ cell_features.append(
287
+ {
288
+ "cell_id": cell["id"],
289
+ "avg_speed": avg_speed,
290
+ "directionality": directionality,
291
+ "msd": msd,
292
+ "track_duration": duration,
293
+ "track_length": len(cell["positions"]),
294
+ }
295
+ )
296
+
297
+ # Step 5: Cluster cells based on motility features
298
+ if len(cell_features) < num_clusters:
299
+ return f"Error: Not enough cells ({len(cell_features)}) to form {num_clusters} clusters. Try reducing num_clusters."
300
+
301
+ # Create feature matrix for clustering
302
+ feature_df = pd.DataFrame(cell_features)
303
+ feature_matrix = feature_df[["avg_speed", "directionality", "msd"]].values
304
+
305
+ # Normalize features
306
+ feature_matrix = (feature_matrix - np.mean(feature_matrix, axis=0)) / np.std(feature_matrix, axis=0)
307
+
308
+ # Perform k-means clustering
309
+ kmeans = KMeans(n_clusters=num_clusters, random_state=42)
310
+ clusters = kmeans.fit_predict(feature_matrix)
311
+
312
+ # Add cluster assignments to dataframe
313
+ feature_df["cluster"] = clusters
314
+
315
+ # Save results to CSV
316
+ results_file = os.path.join(output_dir, "cell_motility_features.csv")
317
+ feature_df.to_csv(results_file, index=False)
318
+
319
+ # Calculate cluster statistics
320
+ cluster_stats = feature_df.groupby("cluster").agg(
321
+ {
322
+ "avg_speed": ["mean", "std"],
323
+ "directionality": ["mean", "std"],
324
+ "msd": ["mean", "std"],
325
+ "cell_id": "count",
326
+ }
327
+ )
328
+
329
+ # Save cluster statistics
330
+ stats_file = os.path.join(output_dir, "cluster_statistics.csv")
331
+ cluster_stats.to_csv(stats_file)
332
+
333
+ # Create research log
334
+ log = f"""
335
+ Cell Motility Quantification and Clustering Analysis
336
+ ===================================================
337
+
338
+ Analysis Steps:
339
+ 1. Loaded {len(image_files)} time-lapse microscopy images
340
+ 2. Detected and tracked {len(cells)} initial cells across time frames
341
+ 3. Calculated motility features for {len(cell_features)} cells with sufficient tracking data
342
+ 4. Performed k-means clustering to identify {num_clusters} distinct motility patterns
343
+
344
+ Results Summary:
345
+ - Detected {len(cells)} cells in the first frame
346
+ - Successfully tracked {len(cell_features)} cells across multiple frames
347
+ - Clustered cells into {num_clusters} motility pattern groups
348
+
349
+ Cluster Statistics:
350
+ {cluster_stats.to_string()}
351
+
352
+ Output Files:
353
+ - Cell motility features saved to: {results_file}
354
+ - Cluster statistics saved to: {stats_file}
355
+
356
+ Analysis Notes:
357
+ - Features used for clustering: average speed, directionality, and mean squared displacement
358
+ - Cells were required to be tracked for at least 3 frames to be included in the analysis
359
+ """
360
+
361
+ # Save research log
362
+ log_file = os.path.join(output_dir, "research_log.txt")
363
+ with open(log_file, "w") as f:
364
+ f.write(log)
365
+
366
+ return log
367
+
368
+
369
+ def perform_facs_cell_sorting(
370
+ cell_suspension_data,
371
+ fluorescence_parameter,
372
+ threshold_min=None,
373
+ threshold_max=None,
374
+ output_file="sorted_cells.csv",
375
+ ):
376
+ """Performs Fluorescence-Activated Cell Sorting (FACS) to enrich cell populations based on fluorescence characteristics.
377
+
378
+ Parameters
379
+ ----------
380
+ cell_suspension_data : str
381
+ Path to the FCS file containing flow cytometry data
382
+ fluorescence_parameter : str
383
+ The fluorescence parameter to use for sorting (e.g., 'GFP', 'FITC', 'PE')
384
+ threshold_min : float, optional
385
+ Minimum threshold for the fluorescence parameter. Cells below this value will be excluded
386
+ threshold_max : float, optional
387
+ Maximum threshold for the fluorescence parameter. Cells above this value will be excluded
388
+ output_file : str, optional
389
+ Filename to save the sorted cell population data
390
+
391
+ Returns
392
+ -------
393
+ str
394
+ Research log detailing the FACS cell sorting process
395
+
396
+ """
397
+ import os
398
+
399
+ import pandas as pd
400
+
401
+ # Initialize research log
402
+ log = "# FACS-based Cell Sorting and Enrichment Research Log\n\n"
403
+
404
+ try:
405
+ # Load cell suspension data
406
+ log += "## Loading Cell Suspension Data\n"
407
+ if isinstance(cell_suspension_data, str):
408
+ # If data is provided as a file path
409
+ if cell_suspension_data.lower().endswith(".fcs"):
410
+ try:
411
+ import flowkit as fk
412
+
413
+ fcs_data = fk.Sample(cell_suspension_data)
414
+ cell_df = pd.DataFrame(fcs_data.get_dataframe())
415
+ log += f"Successfully loaded FCS file containing {len(cell_df)} cells\n"
416
+ except ImportError:
417
+ # Fallback if flowkit is not available
418
+ log += "FlowKit not available, attempting to load as CSV\n"
419
+ cell_df = pd.read_csv(cell_suspension_data)
420
+ else:
421
+ # Assume CSV or other tabular format
422
+ cell_df = pd.read_csv(cell_suspension_data)
423
+ log += f"Loaded data file containing {len(cell_df)} cells\n"
424
+ else:
425
+ # Assume data is already a DataFrame
426
+ cell_df = cell_suspension_data.copy()
427
+ log += f"Using provided DataFrame containing {len(cell_df)} cells\n"
428
+
429
+ # Validate fluorescence parameter exists in data
430
+ if fluorescence_parameter not in cell_df.columns:
431
+ raise ValueError(f"Fluorescence parameter '{fluorescence_parameter}' not found in data")
432
+
433
+ # Apply gating strategy based on fluorescence thresholds
434
+ log += f"\n## Applying Gating Strategy for {fluorescence_parameter}\n"
435
+ original_count = len(cell_df)
436
+
437
+ # Filter cells based on min/max thresholds
438
+ if threshold_min is not None:
439
+ cell_df = cell_df[cell_df[fluorescence_parameter] >= threshold_min]
440
+ log += f"Applied minimum threshold of {threshold_min}: {len(cell_df)} cells remaining\n"
441
+
442
+ if threshold_max is not None:
443
+ cell_df = cell_df[cell_df[fluorescence_parameter] <= threshold_max]
444
+ log += f"Applied maximum threshold of {threshold_max}: {len(cell_df)} cells remaining\n"
445
+
446
+ # Calculate enrichment statistics
447
+ enrichment_percent = (len(cell_df) / original_count) * 100 if original_count > 0 else 0
448
+ log += "\n## Enrichment Results\n"
449
+ log += f"Original population: {original_count} cells\n"
450
+ log += f"Sorted population: {len(cell_df)} cells\n"
451
+ log += f"Enrichment percentage: {enrichment_percent:.2f}%\n"
452
+
453
+ # Calculate statistics of sorted population
454
+ mean_fluorescence = cell_df[fluorescence_parameter].mean()
455
+ median_fluorescence = cell_df[fluorescence_parameter].median()
456
+
457
+ log += f"Mean {fluorescence_parameter}: {mean_fluorescence:.2f}\n"
458
+ log += f"Median {fluorescence_parameter}: {median_fluorescence:.2f}\n"
459
+
460
+ # Save sorted population to file
461
+ cell_df.to_csv(output_file, index=False)
462
+ log += "\n## Output\n"
463
+ log += f"Sorted cell population saved to: {os.path.abspath(output_file)}\n"
464
+
465
+ return log
466
+
467
+ except Exception as e:
468
+ log += "\n## Error\n"
469
+ log += f"An error occurred during cell sorting: {str(e)}\n"
470
+ return log
471
+
472
+
473
+ def analyze_flow_cytometry_immunophenotyping(
474
+ fcs_file_path, gating_strategy, compensation_matrix=None, output_dir="./results"
475
+ ):
476
+ """Analyze flow cytometry data to identify and quantify specific cell populations based on surface markers.
477
+
478
+ Parameters
479
+ ----------
480
+ fcs_file_path : str
481
+ Path to the FCS file containing flow cytometry data
482
+ gating_strategy : dict
483
+ Dictionary defining the gating strategy. Each key is a population name, and each value is a list of tuples
484
+ (marker, operator, threshold). For example: {'HSCs': [('Lin', '<', 100), ('Sca1', '>', 1000), ...]}
485
+ compensation_matrix : numpy.ndarray, optional
486
+ Spillover/compensation matrix to correct for fluorescence overlap
487
+ output_dir : str, optional
488
+ Directory to save the results
489
+
490
+ Returns
491
+ -------
492
+ str
493
+ Research log summarizing the analysis steps and results
494
+
495
+ """
496
+ import os
497
+
498
+ import pandas as pd
499
+ from FlowCytometryTools import FCMeasurement
500
+
501
+ # Create output directory if it doesn't exist
502
+ if not os.path.exists(output_dir):
503
+ os.makedirs(output_dir)
504
+
505
+ # Load the FCS file
506
+ sample = FCMeasurement(ID="Sample", datafile=fcs_file_path)
507
+
508
+ # Apply compensation if provided
509
+ if compensation_matrix is not None:
510
+ sample = sample.compensate(compensation_matrix)
511
+
512
+ # Initialize log
513
+ log = "Flow Cytometry Analysis Log\n"
514
+ log += "==========================\n"
515
+ log += f"File analyzed: {os.path.basename(fcs_file_path)}\n"
516
+ log += f"Total events: {len(sample)}\n\n"
517
+
518
+ # Create a dictionary to store population counts
519
+ populations = {}
520
+
521
+ # Apply gating strategy to identify cell populations
522
+ for pop_name, gates in gating_strategy.items():
523
+ # Start with all events
524
+ current_population = sample.copy()
525
+
526
+ log += f"Identifying {pop_name} population:\n"
527
+
528
+ # Apply each gate sequentially
529
+ for marker, operator, threshold in gates:
530
+ initial_count = len(current_population)
531
+
532
+ if operator == ">":
533
+ current_population = current_population.gate(f"{marker} > {threshold}")
534
+ elif operator == "<":
535
+ current_population = current_population.gate(f"{marker} < {threshold}")
536
+ elif operator == "between":
537
+ # For 'between', threshold should be a tuple (lower, upper)
538
+ lower, upper = threshold
539
+ current_population = current_population.gate(f"{marker} > {lower} and {marker} < {upper}")
540
+
541
+ final_count = len(current_population)
542
+ log += f" Gate {marker} {operator} {threshold}: {initial_count} → {final_count} events\n"
543
+
544
+ # Store the final population
545
+ populations[pop_name] = current_population
546
+
547
+ # Calculate percentage of original sample
548
+ percentage = (len(current_population) / len(sample)) * 100
549
+ log += f" Final {pop_name} count: {len(current_population)} events ({percentage:.2f}% of total)\n\n"
550
+
551
+ # Create a summary dataframe
552
+ summary_data = {"Population": [], "Count": [], "Percentage": []}
553
+
554
+ for pop_name, population in populations.items():
555
+ summary_data["Population"].append(pop_name)
556
+ summary_data["Count"].append(len(population))
557
+ summary_data["Percentage"].append((len(population) / len(sample)) * 100)
558
+
559
+ summary_df = pd.DataFrame(summary_data)
560
+
561
+ # Save the summary to a CSV file
562
+ summary_file = os.path.join(output_dir, "population_summary.csv")
563
+ summary_df.to_csv(summary_file, index=False)
564
+
565
+ log += "Summary of identified populations:\n"
566
+ for _, row in summary_df.iterrows():
567
+ log += f" {row['Population']}: {row['Count']} events ({row['Percentage']:.2f}%)\n"
568
+
569
+ log += f"\nDetailed results saved to: {summary_file}\n"
570
+
571
+ return log
572
+
573
+
574
+ def analyze_mitochondrial_morphology_and_potential(morphology_image_path, potential_image_path, output_dir="./output"):
575
+ """Quantifies metrics of mitochondrial morphology and membrane potential from fluorescence microscopy images.
576
+
577
+ Parameters
578
+ ----------
579
+ morphology_image_path : str
580
+ Path to the fluorescence microscopy image showing mitochondrial morphology (e.g., MTS-GFP)
581
+ potential_image_path : str
582
+ Path to the fluorescence microscopy image showing mitochondrial membrane potential (e.g., TMRE staining)
583
+ output_dir : str, optional
584
+ Directory to save output files, default is "./output"
585
+
586
+ Returns
587
+ -------
588
+ str
589
+ A research log summarizing the analysis steps and results
590
+
591
+ """
592
+ import datetime
593
+ import os
594
+
595
+ import cv2
596
+ import numpy as np
597
+ from scipy import ndimage
598
+ from skimage import filters, io, morphology, util
599
+
600
+ # Create output directory if it doesn't exist
601
+ os.makedirs(output_dir, exist_ok=True)
602
+
603
+ log = []
604
+ log.append(f"Mitochondrial Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
605
+ log.append("=" * 50)
606
+
607
+ # Load images
608
+ try:
609
+ morph_img = io.imread(morphology_image_path)
610
+ pot_img = io.imread(potential_image_path)
611
+
612
+ log.append(f"Successfully loaded morphology image: {morphology_image_path}")
613
+ log.append(f"Successfully loaded potential image: {potential_image_path}")
614
+ log.append(f"Morphology image shape: {morph_img.shape}")
615
+ log.append(f"Potential image shape: {pot_img.shape}")
616
+ except Exception as e:
617
+ return f"Error loading images: {str(e)}"
618
+
619
+ # Convert to grayscale if needed
620
+ if len(morph_img.shape) > 2:
621
+ morph_img = cv2.cvtColor(morph_img, cv2.COLOR_RGB2GRAY)
622
+ log.append("Converted morphology image to grayscale")
623
+ if len(pot_img.shape) > 2:
624
+ pot_img = cv2.cvtColor(pot_img, cv2.COLOR_RGB2GRAY)
625
+ log.append("Converted potential image to grayscale")
626
+
627
+ # Normalize images
628
+ morph_img = util.img_as_float(morph_img)
629
+ pot_img = util.img_as_float(pot_img)
630
+
631
+ log.append("\nMorphology Analysis:")
632
+ log.append("-" * 30)
633
+
634
+ # Denoise morphology image
635
+ morph_img_denoised = filters.gaussian(morph_img, sigma=1.0)
636
+
637
+ # Threshold to segment mitochondria
638
+ threshold_value = filters.threshold_otsu(morph_img_denoised)
639
+ binary_img = morph_img_denoised > threshold_value
640
+
641
+ # Remove small objects
642
+ binary_img = morphology.remove_small_objects(binary_img, min_size=20)
643
+
644
+ # Save binary image
645
+ binary_path = os.path.join(output_dir, "binary_mitochondria.png")
646
+ io.imsave(binary_path, util.img_as_ubyte(binary_img))
647
+ log.append(f"Binary segmentation saved to: {binary_path}")
648
+
649
+ # Skeletonize for network analysis
650
+ skeleton = morphology.skeletonize(binary_img)
651
+ skeleton_path = os.path.join(output_dir, "skeleton.png")
652
+ io.imsave(skeleton_path, util.img_as_ubyte(skeleton))
653
+ log.append(f"Skeleton image saved to: {skeleton_path}")
654
+
655
+ # Calculate morphology metrics
656
+ # 1. Count branches and junctions
657
+ # Label the skeleton
658
+ labeled_skeleton, num_branches = ndimage.label(skeleton)
659
+
660
+ # Find branch points (pixels with more than 2 neighbors)
661
+ kernel = np.ones((3, 3), dtype=np.uint8)
662
+ kernel[1, 1] = 0 # Don't count the center pixel
663
+ neighbor_count = ndimage.convolve(skeleton.astype(np.uint8), kernel)
664
+ junction_points = (neighbor_count > 2) & skeleton
665
+ num_junctions = np.sum(junction_points)
666
+
667
+ # 2. Calculate fragmentation metrics
668
+ labeled_objects, num_objects = ndimage.label(binary_img)
669
+ object_sizes = ndimage.sum(binary_img, labeled_objects, range(1, num_objects + 1))
670
+ mean_size = np.mean(object_sizes) if len(object_sizes) > 0 else 0
671
+
672
+ # Calculate network connectivity (ratio of junctions to branches)
673
+ connectivity = num_junctions / num_branches if num_branches > 0 else 0
674
+
675
+ # Calculate fragmentation index (inverse of mean object size, normalized)
676
+ fragmentation = 1 / (mean_size / np.max(object_sizes)) if mean_size > 0 and np.max(object_sizes) > 0 else 0
677
+
678
+ log.append(f"Number of mitochondrial fragments: {num_objects}")
679
+ log.append(f"Number of branches: {num_branches}")
680
+ log.append(f"Number of junction points: {num_junctions}")
681
+ log.append(f"Network connectivity index: {connectivity:.4f}")
682
+ log.append(f"Fragmentation index: {fragmentation:.4f}")
683
+ log.append(f"Mean fragment size: {mean_size:.2f} pixels")
684
+
685
+ # Membrane potential analysis
686
+ log.append("\nMembrane Potential Analysis:")
687
+ log.append("-" * 30)
688
+
689
+ # Create mask from morphology image to analyze only mitochondrial regions
690
+ mito_mask = binary_img
691
+
692
+ # Calculate potential metrics within mitochondrial regions
693
+ pot_intensity_raw = np.mean(pot_img)
694
+ pot_intensity_in_mito = np.mean(pot_img[mito_mask]) if np.sum(mito_mask) > 0 else 0
695
+
696
+ # Calculate potential heterogeneity (standard deviation of intensity)
697
+ pot_heterogeneity = np.std(pot_img[mito_mask]) if np.sum(mito_mask) > 0 else 0
698
+
699
+ # Calculate potential distribution
700
+ if np.sum(mito_mask) > 0:
701
+ percentiles = [10, 25, 50, 75, 90]
702
+ pot_percentiles = np.percentile(pot_img[mito_mask], percentiles)
703
+ percentile_str = ", ".join([f"{p}th: {v:.4f}" for p, v in zip(percentiles, pot_percentiles, strict=False)])
704
+ else:
705
+ percentile_str = "No mitochondrial regions detected"
706
+
707
+ log.append(f"Overall mean TMRE intensity: {pot_intensity_raw:.4f}")
708
+ log.append(f"Mean TMRE intensity in mitochondria: {pot_intensity_in_mito:.4f}")
709
+ log.append(f"TMRE intensity heterogeneity (std): {pot_heterogeneity:.4f}")
710
+ log.append(f"TMRE intensity percentiles: {percentile_str}")
711
+
712
+ # Create a combined results image
713
+ # Overlay potential intensity on morphology
714
+ overlay = np.zeros((morph_img.shape[0], morph_img.shape[1], 3), dtype=np.float32)
715
+ overlay[..., 0] = morph_img # Red channel - morphology
716
+ overlay[..., 2] = pot_img # Blue channel - potential
717
+ overlay = np.clip(overlay, 0, 1)
718
+
719
+ overlay_path = os.path.join(output_dir, "morphology_potential_overlay.png")
720
+ io.imsave(overlay_path, util.img_as_ubyte(overlay))
721
+ log.append(f"Overlay image saved to: {overlay_path}")
722
+
723
+ # Save quantitative results to CSV
724
+ import csv
725
+
726
+ results_path = os.path.join(output_dir, "mitochondrial_analysis_results.csv")
727
+ with open(results_path, "w", newline="") as csvfile:
728
+ writer = csv.writer(csvfile)
729
+ writer.writerow(["Metric", "Value"])
730
+ writer.writerow(["Number of fragments", num_objects])
731
+ writer.writerow(["Number of branches", num_branches])
732
+ writer.writerow(["Number of junctions", num_junctions])
733
+ writer.writerow(["Network connectivity", connectivity])
734
+ writer.writerow(["Fragmentation index", fragmentation])
735
+ writer.writerow(["Mean fragment size", mean_size])
736
+ writer.writerow(["Mean TMRE intensity", pot_intensity_in_mito])
737
+ writer.writerow(["TMRE heterogeneity", pot_heterogeneity])
738
+
739
+ log.append(f"Quantitative results saved to: {results_path}")
740
+ log.append("\nAnalysis complete.")
741
+
742
+ return "\n".join(log)
Biomanus_upload/biomni/tool/database.py ADDED
The diff for this file is too large to render. See raw diff