ZhiyuanChen commited on
Commit
915fbd6
·
unverified ·
1 Parent(s): 36a91e2

implement regulatory-variant-effect app

Browse files

Signed-off-by: Zhiyuan Chen <this@zyc.ai>

Files changed (5) hide show
  1. .gitignore +449 -0
  2. .pre-commit-config.yaml +51 -0
  3. README.md +26 -6
  4. app.py +416 -0
  5. requirements.txt +5 -0
.gitignore ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Ignore Visual Studio temporary files, build results, and
2
+ ## files generated by popular Visual Studio add-ons.
3
+ ##
4
+ ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5
+
6
+ # User-specific files
7
+ *.rsuser
8
+ *.suo
9
+ *.user
10
+ *.userosscache
11
+ *.sln.docstates
12
+
13
+ # User-specific files (MonoDevelop/Xamarin Studio)
14
+ *.userprefs
15
+
16
+ # Mono auto generated files
17
+ mono_crash.*
18
+
19
+ # Build results
20
+ [Dd]ebug/
21
+ [Dd]ebugPublic/
22
+ [Rr]elease/
23
+ [Rr]eleases/
24
+ x64/
25
+ x86/
26
+ [Ww][Ii][Nn]32/
27
+ [Aa][Rr][Mm]/
28
+ [Aa][Rr][Mm]64/
29
+ bld/
30
+ [Bb]in/
31
+ [Oo]bj/
32
+ [Ll]og/
33
+ [Ll]ogs/
34
+
35
+ # Visual Studio Code cache/options directory
36
+ .vscode/
37
+
38
+ # Visual Studio 2015/2017 cache/options directory
39
+ .vs/
40
+ # Uncomment if you have tasks that create the project's static files in wwwroot
41
+ #wwwroot/
42
+
43
+ # JetBrains
44
+ .idea/
45
+
46
+ # Visual Studio 2017 auto generated files
47
+ Generated\ Files/
48
+
49
+ # MSTest test Results
50
+ [Tt]est[Rr]esult*/
51
+ [Bb]uild[Ll]og.*
52
+
53
+ # NUnit
54
+ *.VisualState.xml
55
+ TestResult.xml
56
+ nunit-*.xml
57
+
58
+ # Build Results of an ATL Project
59
+ [Dd]ebugPS/
60
+ [Rr]eleasePS/
61
+ dlldata.c
62
+
63
+ # Benchmark Results
64
+ BenchmarkDotNet.Artifacts/
65
+
66
+ # .NET Core
67
+ project.lock.json
68
+ project.fragment.lock.json
69
+ artifacts/
70
+
71
+ # ASP.NET Scaffolding
72
+ ScaffoldingReadMe.txt
73
+
74
+ # StyleCop
75
+ StyleCopReport.xml
76
+
77
+ # Files built by Visual Studio
78
+ *_i.c
79
+ *_p.c
80
+ *_h.h
81
+ *.ilk
82
+ *.meta
83
+ *.obj
84
+ *.iobj
85
+ *.pch
86
+ *.pdb
87
+ *.ipdb
88
+ *.pgc
89
+ *.pgd
90
+ *.rsp
91
+ *.sbr
92
+ *.tlb
93
+ *.tli
94
+ *.tlh
95
+ *.tmp
96
+ *.tmp_proj
97
+ *_wpftmp.csproj
98
+ *.log
99
+ *.tlog
100
+ *.vspscc
101
+ *.vssscc
102
+ .builds
103
+ *.pidb
104
+ *.svclog
105
+ *.scc
106
+
107
+ # Chutzpah Test files
108
+ _Chutzpah*
109
+
110
+ # Visual C++ cache files
111
+ ipch/
112
+ *.aps
113
+ *.ncb
114
+ *.opendb
115
+ *.opensdf
116
+ *.sdf
117
+ *.cachefile
118
+ *.VC.db
119
+ *.VC.VC.opendb
120
+
121
+ # Visual Studio profiler
122
+ *.psess
123
+ *.vsp
124
+ *.vspx
125
+ *.sap
126
+
127
+ # Visual Studio Trace Files
128
+ *.e2e
129
+
130
+ # TFS 2012 Local Workspace
131
+ $tf/
132
+
133
+ # Guidance Automation Toolkit
134
+ *.gpState
135
+
136
+ # ReSharper is a .NET coding add-in
137
+ _ReSharper*/
138
+ *.[Rr]e[Ss]harper
139
+ *.DotSettings.user
140
+
141
+ # TeamCity is a build add-in
142
+ _TeamCity*
143
+
144
+ # DotCover is a Code Coverage Tool
145
+ *.dotCover
146
+
147
+ # AxoCover is a Code Coverage Tool
148
+ .axoCover/*
149
+ !.axoCover/settings.json
150
+
151
+ # Coverlet is a free, cross platform Code Coverage Tool
152
+ coverage*.json
153
+ coverage*.xml
154
+ coverage*.info
155
+
156
+ # Visual Studio code coverage results
157
+ *.coverage
158
+ *.coveragexml
159
+
160
+ # NCrunch
161
+ _NCrunch_*
162
+ .*crunch*.local.xml
163
+ nCrunchTemp_*
164
+
165
+ # MightyMoose
166
+ *.mm.*
167
+ AutoTest.Net/
168
+
169
+ # Web workbench (sass)
170
+ .sass-cache/
171
+
172
+ # Installshield output folder
173
+ [Ee]xpress/
174
+
175
+ # DocProject is a documentation generator add-in
176
+ DocProject/buildhelp/
177
+ DocProject/Help/*.HxT
178
+ DocProject/Help/*.HxC
179
+ DocProject/Help/*.hhc
180
+ DocProject/Help/*.hhk
181
+ DocProject/Help/*.hhp
182
+ DocProject/Help/Html2
183
+ DocProject/Help/html
184
+
185
+ # Click-Once directory
186
+ publish/
187
+
188
+ # Publish Web Output
189
+ *.[Pp]ublish.xml
190
+ *.azurePubxml
191
+ # Note: Comment the next line if you want to checkin your web deploy settings,
192
+ # but database connection strings (with potential passwords) will be unencrypted
193
+ *.pubxml
194
+ *.publishproj
195
+
196
+ # Microsoft Azure Web App publish settings. Comment the next line if you want to
197
+ # checkin your Azure Web App publish settings, but sensitive information contained
198
+ # in these scripts will be unencrypted
199
+ PublishScripts/
200
+
201
+ # NuGet Packages
202
+ *.nupkg
203
+ # NuGet Symbol Packages
204
+ *.snupkg
205
+ # The packages folder can be ignored because of Package Restore
206
+ **/[Pp]ackages/*
207
+ # except build/, which is used as an MSBuild target.
208
+ !**/[Pp]ackages/build/
209
+ # Uncomment if necessary however generally it will be regenerated when needed
210
+ #!**/[Pp]ackages/repositories.config
211
+ # NuGet v3's project.json files produces more ignorable files
212
+ *.nuget.props
213
+ *.nuget.targets
214
+
215
+ # Microsoft Azure Build Output
216
+ csx/
217
+ *.build.csdef
218
+
219
+ # Microsoft Azure Emulator
220
+ ecf/
221
+ rcf/
222
+
223
+ # Windows Store app package directories and files
224
+ AppPackages/
225
+ BundleArtifacts/
226
+ Package.StoreAssociation.xml
227
+ _pkginfo.txt
228
+ *.appx
229
+ *.appxbundle
230
+ *.appxupload
231
+
232
+ # Visual Studio cache files
233
+ # files ending in .cache can be ignored
234
+ *.[Cc]ache
235
+ # but keep track of directories ending in .cache
236
+ !?*.[Cc]ache/
237
+
238
+ # Others
239
+ ClientBin/
240
+ ~$*
241
+ *~
242
+ *.dbmdl
243
+ *.dbproj.schemaview
244
+ *.jfm
245
+ *.pfx
246
+ *.publishsettings
247
+ orleans.codegen.cs
248
+
249
+ # Including strong name files can present a security risk
250
+ # (https://github.com/github/gitignore/pull/2483#issue-259490424)
251
+ #*.snk
252
+
253
+ # Since there are multiple workflows, uncomment next line to ignore bower_components
254
+ # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
255
+ #bower_components/
256
+
257
+ # RIA/Silverlight projects
258
+ Generated_Code/
259
+
260
+ # Backup & report files from converting an old project file
261
+ # to a newer Visual Studio version. Backup files are not needed,
262
+ # because we have git ;-)
263
+ _UpgradeReport_Files/
264
+ Backup*/
265
+ UpgradeLog*.XML
266
+ UpgradeLog*.htm
267
+ ServiceFabricBackup/
268
+ *.rptproj.bak
269
+
270
+ # SQL Server files
271
+ *.mdf
272
+ *.ldf
273
+ *.ndf
274
+
275
+ # Business Intelligence projects
276
+ *.rdl.data
277
+ *.bim.layout
278
+ *.bim_*.settings
279
+ *.rptproj.rsuser
280
+ *- [Bb]ackup.rdl
281
+ *- [Bb]ackup ([0-9]).rdl
282
+ *- [Bb]ackup ([0-9][0-9]).rdl
283
+
284
+ # Microsoft Fakes
285
+ FakesAssemblies/
286
+
287
+ # GhostDoc plugin setting file
288
+ *.GhostDoc.xml
289
+
290
+ # Node.js Tools for Visual Studio
291
+ .ntvs_analysis.dat
292
+ node_modules/
293
+
294
+ # Visual Studio 6 build log
295
+ *.plg
296
+
297
+ # Visual Studio 6 workspace options file
298
+ *.opt
299
+
300
+ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
301
+ *.vbw
302
+
303
+ # Visual Studio 6 auto-generated project file (contains which files were open etc.)
304
+ *.vbp
305
+
306
+ # Visual Studio 6 workspace and project file (working project files containing files to include in project)
307
+ *.dsw
308
+ *.dsp
309
+
310
+ # Visual Studio 6 technical files
311
+ *.ncb
312
+ *.aps
313
+
314
+ # Visual Studio LightSwitch build output
315
+ **/*.HTMLClient/GeneratedArtifacts
316
+ **/*.DesktopClient/GeneratedArtifacts
317
+ **/*.DesktopClient/ModelManifest.xml
318
+ **/*.Server/GeneratedArtifacts
319
+ **/*.Server/ModelManifest.xml
320
+ _Pvt_Extensions
321
+
322
+ # Paket dependency manager
323
+ .paket/paket.exe
324
+ paket-files/
325
+
326
+ # FAKE - F# Make
327
+ .fake/
328
+
329
+ # CodeRush personal settings
330
+ .cr/personal
331
+
332
+ # Python Tools for Visual Studio (PTVS)
333
+ __pycache__/
334
+ *.pyc
335
+
336
+ # Cake - Uncomment if you are using it
337
+ # tools/**
338
+ # !tools/packages.config
339
+
340
+ # Tabs Studio
341
+ *.tss
342
+
343
+ # Telerik's JustMock configuration file
344
+ *.jmconfig
345
+
346
+ # BizTalk build output
347
+ *.btp.cs
348
+ *.btm.cs
349
+ *.odx.cs
350
+ *.xsd.cs
351
+
352
+ # OpenCover UI analysis results
353
+ OpenCover/
354
+
355
+ # Azure Stream Analytics local run output
356
+ ASALocalRun/
357
+
358
+ # MSBuild Binary and Structured Log
359
+ *.binlog
360
+
361
+ # NVidia Nsight GPU debugger configuration file
362
+ *.nvuser
363
+
364
+ # MFractors (Xamarin productivity tool) working folder
365
+ .mfractor/
366
+
367
+ # Local History for Visual Studio
368
+ .localhistory/
369
+
370
+ # Visual Studio History (VSHistory) files
371
+ .vshistory/
372
+
373
+ # BeatPulse healthcheck temp database
374
+ healthchecksdb
375
+
376
+ # Backup folder for Package Reference Convert tool in Visual Studio 2017
377
+ MigrationBackup/
378
+
379
+ # Ionide (cross platform F# VS Code tools) working folder
380
+ .ionide/
381
+
382
+ # Fody - auto-generated XML schema
383
+ FodyWeavers.xsd
384
+
385
+ # VS Code files for those working on multiple tools
386
+ .vscode/*
387
+ !.vscode/settings.json
388
+ !.vscode/tasks.json
389
+ !.vscode/launch.json
390
+ !.vscode/extensions.json
391
+ *.code-workspace
392
+
393
+ # JetBrains
394
+ .idea/
395
+
396
+ # Local History for Visual Studio Code
397
+ .history/
398
+
399
+ # Windows Installer files from build outputs
400
+ *.cab
401
+ *.msi
402
+ *.msix
403
+ *.msm
404
+ *.msp
405
+
406
+ # JetBrains Rider
407
+ *.sln.iml
408
+
409
+ # version
410
+ **/_version.py
411
+
412
+ # Python Egg
413
+ *.egg-info/
414
+
415
+ # Tar
416
+ **/*.tar
417
+ **/*.tgz
418
+ **/*.txz
419
+ **/*.gz
420
+ **/*.xz
421
+
422
+ # Pickle
423
+ **/*.pickle
424
+ **/*.pkl
425
+
426
+ # Checkpoints
427
+ **/*.onnx
428
+ **/*.ckpt
429
+ **/*.safetensors
430
+ **/*.pth
431
+ **/*.pt
432
+ **/*.bin
433
+ **/*.msgpack
434
+ **/*.h5
435
+ **/*.ot
436
+
437
+ # Data
438
+ **/*.np
439
+ **/*.npy
440
+ **/*.npz
441
+ **/*.numpy
442
+ **/*.pd
443
+ **/*.pandas
444
+
445
+ # debug files
446
+ **/*debug*
447
+
448
+ # site
449
+ **/site
.pre-commit-config.yaml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ default_language_version:
2
+ python: python3
3
+ repos:
4
+ - repo: https://github.com/PSF/black
5
+ rev: 25.12.0
6
+ hooks:
7
+ - id: black
8
+ args: [--safe, --quiet, --line-length=120]
9
+ - repo: https://github.com/PyCQA/isort
10
+ rev: 7.0.0
11
+ hooks:
12
+ - id: isort
13
+ name: isort
14
+ args: [--profile=black, --line-length=120]
15
+ - repo: https://github.com/PyCQA/flake8
16
+ rev: 7.3.0
17
+ hooks:
18
+ - id: flake8
19
+ args: [--max-line-length=120]
20
+ additional_dependencies:
21
+ - flake8-bugbear
22
+ - flake8-comprehensions
23
+ - flake8-simplify
24
+ - repo: https://github.com/asottile/pyupgrade
25
+ rev: v3.21.2
26
+ hooks:
27
+ - id: pyupgrade
28
+ args: [--keep-runtime-typing]
29
+ - repo: https://github.com/codespell-project/codespell
30
+ rev: v2.4.1
31
+ hooks:
32
+ - id: codespell
33
+ args: [--ignore-regex=(?i)aparent]
34
+ - repo: https://github.com/pre-commit/pre-commit-hooks
35
+ rev: v6.0.0
36
+ hooks:
37
+ - id: check-added-large-files
38
+ - id: check-ast
39
+ - id: check-builtin-literals
40
+ - id: check-case-conflict
41
+ - id: check-docstring-first
42
+ - id: check-json
43
+ - id: check-toml
44
+ - id: check-yaml
45
+ - id: debug-statements
46
+ - id: end-of-file-fixer
47
+ - id: fix-byte-order-marker
48
+ - id: mixed-line-ending
49
+ args: ["--fix=lf"]
50
+ - id: requirements-txt-fixer
51
+ - id: trailing-whitespace
README.md CHANGED
@@ -1,15 +1,35 @@
1
  ---
2
  title: Regulatory Variant Effect
3
- emoji: 📉
4
- colorFrom: indigo
5
- colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  license: agpl-3.0
12
- short_description: Regulatory Variant Effect
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
1
  ---
2
  title: Regulatory Variant Effect
3
+ emoji: 🧬
4
+ colorFrom: pink
5
+ colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.14.0
8
+ python_version: "3.13"
9
  app_file: app.py
10
  pinned: false
11
  license: agpl-3.0
12
+ suggested_hardware: t4-small
13
+ models:
14
+ - multimolecule/a2zchromatin
15
+ - multimolecule/basset
16
+ - multimolecule/deepmel
17
+ - multimolecule/deepsea
18
+ - multimolecule/deepstarr
19
+ - multimolecule/malinois
20
+ - multimolecule/mpradragonn
21
+ - multimolecule/scbasset
22
+ - multimolecule/xpresso
23
+ tags:
24
+ - biology
25
+ - dna
26
+ - regulatory-genomics
27
+ - variant-effect
28
+ - multimolecule
29
  ---
30
 
31
+ Interactive regulatory variant-effect scoring with MultiMolecule.
32
+
33
+ Enter matched reference and alternative DNA sequences, choose a regulatory variant-effect checkpoint, and inspect the alternative-minus-reference delta table. Sequence-level reference and alternative scores are included when the pipeline returns them. Results can be downloaded as CSV or JSON.
34
+
35
+ This Space intentionally scores only user-provided sequence windows. It does not perform genome-coordinate lookup, reference-genome retrieval, transcript mapping, or variant normalization.
app.py ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MultiMolecule
2
+ # Copyright (C) 2024-Present MultiMolecule
3
+
4
+ # This file is part of MultiMolecule.
5
+
6
+ # MultiMolecule is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # any later version.
10
+
11
+ # MultiMolecule is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ # For additional terms and clarifications, please refer to our License FAQ at:
20
+ # <https://multimolecule.danling.org/about/license-faq>.
21
+
22
+ from __future__ import annotations
23
+
24
+ import csv
25
+ import json
26
+ import re
27
+ import tempfile
28
+ import time
29
+ from functools import lru_cache
30
+ from typing import Any, Mapping
31
+ from urllib.parse import parse_qs, urlparse
32
+
33
+ import gradio as gr
34
+ import matplotlib
35
+ import numpy as np
36
+ import torch
37
+ from transformers import pipeline
38
+
39
+ matplotlib.use("Agg")
40
+
41
+ import matplotlib.pyplot as plt # noqa: E402
42
+ import multimolecule # noqa: E402, F401 - registers MultiMolecule models and pipelines with Transformers
43
+
44
+ DEFAULT_REFERENCE_SEQUENCE = "ACGT" * 250
45
+ DEFAULT_ALTERNATIVE_SEQUENCE = "ACGT" * 125 + "TCGA" + "ACGT" * 124
46
+ DEFAULT_MODEL_LABEL = "DeepSEA"
47
+
48
+ MODEL_OPTIONS = {
49
+ "A2Z Chromatin": "multimolecule/a2zchromatin",
50
+ "Basset": "multimolecule/basset",
51
+ "DeepMEL": "multimolecule/deepmel",
52
+ "DeepSEA": "multimolecule/deepsea",
53
+ "DeepSTARR": "multimolecule/deepstarr",
54
+ "Malinois": "multimolecule/malinois",
55
+ "MPRA-DragoNN": "multimolecule/mpradragonn",
56
+ "scBasset": "multimolecule/scbasset",
57
+ "Xpresso": "multimolecule/xpresso",
58
+ }
59
+ MODEL_LABELS = {model_id: label for label, model_id in MODEL_OPTIONS.items()}
60
+
61
+ TABLE_HEADERS = ["position", "nucleotide", "channel", "delta_score", "reference_score", "alternative_score"]
62
+ DNA_ALPHABET = set("ACGTN")
63
+ FLOAT_PATTERN = re.compile(r"[-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[eE][-+]?\d+)?")
64
+
65
+
66
+ def _device() -> int:
67
+ return 0 if torch.cuda.is_available() else -1
68
+
69
+
70
+ @lru_cache(maxsize=2)
71
+ def load_predictor(model_id: str):
72
+ return pipeline("regulatory-variant-effect", model=model_id, device=_device())
73
+
74
+
75
+ def clean_sequence(sequence: str, label: str) -> str:
76
+ sequence = "".join(str(sequence or "").split()).upper().replace("U", "T")
77
+ if not sequence:
78
+ raise gr.Error(f"{label} sequence is empty.")
79
+ invalid = sorted(set(sequence) - DNA_ALPHABET)
80
+ if invalid:
81
+ invalid_text = ", ".join(invalid)
82
+ raise gr.Error(f"{label} sequence contains unsupported symbols: {invalid_text}. Use A, C, G, T, or N.")
83
+ return sequence
84
+
85
+
86
+ def parse_features(features_text: str) -> Any | None:
87
+ text = str(features_text or "").strip()
88
+ if not text:
89
+ return None
90
+
91
+ try:
92
+ parsed = json.loads(text)
93
+ except json.JSONDecodeError:
94
+ values = FLOAT_PATTERN.findall(text)
95
+ if not values:
96
+ raise gr.Error("Features must be JSON or comma/space-separated numbers.")
97
+ return [float(value) for value in values]
98
+
99
+ if isinstance(parsed, Mapping):
100
+ for key in ("features", "values", "reference_features", "alternative_features"):
101
+ if key in parsed:
102
+ return parsed[key]
103
+ if all(isinstance(value, int | float) for value in parsed.values()):
104
+ return list(parsed.values())
105
+ raise gr.Error("Feature JSON objects must contain a features/values list or only numeric values.")
106
+ if isinstance(parsed, str):
107
+ return parse_features(parsed)
108
+ return parsed
109
+
110
+
111
+ def feature_summary(features: Any | None) -> dict[str, Any]:
112
+ if features is None:
113
+ return {"provided": False}
114
+ try:
115
+ array = np.asarray(features, dtype=float)
116
+ except (TypeError, ValueError):
117
+ return {"provided": True, "shape": None}
118
+ return {"provided": True, "shape": list(array.shape)}
119
+
120
+
121
+ def unpack_prediction_result(result: Any) -> dict[str, Any]:
122
+ if isinstance(result, list):
123
+ if len(result) != 1:
124
+ raise gr.Error(f"Expected one prediction result, got {len(result)}.")
125
+ result = result[0]
126
+ if not isinstance(result, dict):
127
+ raise gr.Error(f"Expected a prediction dictionary, got {type(result).__name__}.")
128
+ return result
129
+
130
+
131
+ def build_delta_rows(result: Mapping[str, Any]) -> list[dict[str, Any]]:
132
+ if "delta_score" in result:
133
+ return [
134
+ {
135
+ "position": "",
136
+ "nucleotide": "",
137
+ "channel": "score",
138
+ "delta_score": result.get("delta_score"),
139
+ "reference_score": result.get("reference_score", ""),
140
+ "alternative_score": result.get("alternative_score", ""),
141
+ }
142
+ ]
143
+
144
+ delta_scores = result.get("delta_scores")
145
+ if isinstance(delta_scores, Mapping):
146
+ reference_scores = result.get("reference_scores") if isinstance(result.get("reference_scores"), Mapping) else {}
147
+ alternative_scores = (
148
+ result.get("alternative_scores") if isinstance(result.get("alternative_scores"), Mapping) else {}
149
+ )
150
+ return [
151
+ {
152
+ "position": "",
153
+ "nucleotide": "",
154
+ "channel": str(channel),
155
+ "delta_score": value,
156
+ "reference_score": reference_scores.get(channel, ""),
157
+ "alternative_score": alternative_scores.get(channel, ""),
158
+ }
159
+ for channel, value in delta_scores.items()
160
+ ]
161
+
162
+ if isinstance(delta_scores, list):
163
+ return build_axis_delta_rows(result, delta_scores)
164
+
165
+ raise gr.Error("The selected model did not return delta scores.")
166
+
167
+
168
+ def build_axis_delta_rows(result: Mapping[str, Any], delta_scores: list[Any]) -> list[dict[str, Any]]:
169
+ channels = [str(channel) for channel in result.get("channels", [])]
170
+ reference_scores = _index_axis_rows(result.get("reference_scores"))
171
+ alternative_scores = _index_axis_rows(result.get("alternative_scores"))
172
+ output_rows: list[dict[str, Any]] = []
173
+
174
+ for row_index, row in enumerate(delta_scores):
175
+ if not isinstance(row, Mapping):
176
+ continue
177
+ position = row.get("position", row.get("bin", row_index))
178
+ channel_names = channels or [
179
+ str(key) for key in row if key not in {"position", "bin", "nucleotide"} and _is_number(row[key])
180
+ ]
181
+ ref_row = reference_scores.get(position, {})
182
+ alt_row = alternative_scores.get(position, {})
183
+ for channel in channel_names:
184
+ if channel not in row:
185
+ continue
186
+ output_rows.append(
187
+ {
188
+ "position": position,
189
+ "nucleotide": row.get("nucleotide", ""),
190
+ "channel": channel,
191
+ "delta_score": row[channel],
192
+ "reference_score": ref_row.get(channel, ""),
193
+ "alternative_score": alt_row.get(channel, ""),
194
+ }
195
+ )
196
+ return output_rows
197
+
198
+
199
+ def _index_axis_rows(rows: Any) -> dict[Any, Mapping[str, Any]]:
200
+ if not isinstance(rows, list):
201
+ return {}
202
+ indexed = {}
203
+ for row_index, row in enumerate(rows):
204
+ if isinstance(row, Mapping):
205
+ indexed[row.get("position", row.get("bin", row_index))] = row
206
+ return indexed
207
+
208
+
209
+ def _is_number(value: Any) -> bool:
210
+ return isinstance(value, int | float | np.number)
211
+
212
+
213
+ def table_values(rows: list[Mapping[str, Any]]) -> list[list[Any]]:
214
+ return [[row.get(header, "") for header in TABLE_HEADERS] for row in rows]
215
+
216
+
217
+ def plot_delta_rows(rows: list[Mapping[str, Any]], max_bars: int = 24):
218
+ numeric_rows = [row for row in rows if _is_number(row.get("delta_score"))]
219
+ fig, ax = plt.subplots(figsize=(7.0, 2.4))
220
+ if not numeric_rows:
221
+ ax.text(0.5, 0.5, "No numeric delta scores", ha="center", va="center", transform=ax.transAxes)
222
+ ax.set_axis_off()
223
+ fig.tight_layout()
224
+ return fig
225
+
226
+ top_rows = sorted(numeric_rows, key=lambda row: abs(float(row["delta_score"])), reverse=True)[:max_bars]
227
+ labels = [_row_label(row) for row in top_rows]
228
+ values = [float(row["delta_score"]) for row in top_rows]
229
+ colors = ["#1b9e77" if value >= 0 else "#d95f02" for value in values]
230
+
231
+ height = min(7.0, max(2.4, 0.28 * len(top_rows) + 1.2))
232
+ fig.set_size_inches(7.0, height, forward=True)
233
+ ax.barh(range(len(top_rows)), values, color=colors)
234
+ ax.axvline(0, color="#333333", linewidth=0.8)
235
+ ax.set_yticks(range(len(top_rows)), labels)
236
+ ax.invert_yaxis()
237
+ ax.set_xlabel("Alternative - reference")
238
+ ax.set_title("Largest absolute delta scores")
239
+ ax.tick_params(axis="y", labelsize=8)
240
+ fig.tight_layout()
241
+ return fig
242
+
243
+
244
+ def _row_label(row: Mapping[str, Any]) -> str:
245
+ channel = str(row.get("channel", "score"))
246
+ position = row.get("position")
247
+ if position not in ("", None):
248
+ nucleotide = row.get("nucleotide")
249
+ suffix = f" {nucleotide}" if nucleotide not in ("", None) else ""
250
+ return f"{position}{suffix} {channel}"
251
+ return channel
252
+
253
+
254
+ def write_result_files(
255
+ model_id: str,
256
+ result: Mapping[str, Any],
257
+ rows: list[Mapping[str, Any]],
258
+ metadata: Mapping[str, Any],
259
+ ) -> tuple[str, str]:
260
+ csv_file = tempfile.NamedTemporaryFile("w", suffix=".csv", newline="", delete=False)
261
+ writer = csv.DictWriter(csv_file, fieldnames=TABLE_HEADERS)
262
+ writer.writeheader()
263
+ writer.writerows({header: row.get(header, "") for header in TABLE_HEADERS} for row in rows)
264
+ csv_file.close()
265
+
266
+ json_file = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False)
267
+ json.dump(
268
+ {
269
+ "metadata": dict(metadata),
270
+ "model": model_id,
271
+ "result": result,
272
+ "delta_table": [{header: row.get(header, "") for header in TABLE_HEADERS} for row in rows],
273
+ },
274
+ json_file,
275
+ indent=2,
276
+ default=_json_default,
277
+ )
278
+ json_file.close()
279
+ return csv_file.name, json_file.name
280
+
281
+
282
+ def _json_default(value: Any):
283
+ if isinstance(value, np.generic):
284
+ return value.item()
285
+ if isinstance(value, np.ndarray):
286
+ return value.tolist()
287
+ raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
288
+
289
+
290
+ def predict(
291
+ model_label: str,
292
+ reference_sequence: str,
293
+ alternative_sequence: str,
294
+ reference_features_text: str,
295
+ alternative_features_text: str,
296
+ ):
297
+ model_id = MODEL_OPTIONS[model_label]
298
+ reference_sequence = clean_sequence(reference_sequence, "Reference")
299
+ alternative_sequence = clean_sequence(alternative_sequence, "Alternative")
300
+ if len(reference_sequence) != len(alternative_sequence):
301
+ raise gr.Error(
302
+ f"Reference and alternative sequences must have the same length. "
303
+ f"Got {len(reference_sequence)} and {len(alternative_sequence)}."
304
+ )
305
+
306
+ reference_features = parse_features(reference_features_text)
307
+ alternative_features = parse_features(alternative_features_text)
308
+ started = time.perf_counter()
309
+
310
+ predictor = load_predictor(model_id)
311
+ try:
312
+ result = predictor(
313
+ reference_sequence,
314
+ alternative=alternative_sequence,
315
+ features=reference_features,
316
+ alternative_features=alternative_features,
317
+ )
318
+ except Exception as error:
319
+ raise gr.Error(f"Prediction failed for {model_id}: {error}") from error
320
+
321
+ result = unpack_prediction_result(result)
322
+ rows = build_delta_rows(result)
323
+ if not rows:
324
+ raise gr.Error("The selected model returned no tabular delta scores.")
325
+
326
+ metadata = {
327
+ "task": "regulatory-variant-effect",
328
+ "model": model_id,
329
+ "device": "cuda" if torch.cuda.is_available() else "cpu",
330
+ "reference_length": len(reference_sequence),
331
+ "alternative_length": len(alternative_sequence),
332
+ "reference_features": feature_summary(reference_features),
333
+ "alternative_features": feature_summary(alternative_features),
334
+ "alternative_features_inherit_reference": alternative_features is None and reference_features is not None,
335
+ "score_definition": "alternative_minus_reference",
336
+ "num_delta_rows": len(rows),
337
+ "has_reference_scores": any(row.get("reference_score") not in ("", None) for row in rows),
338
+ "has_alternative_scores": any(row.get("alternative_score") not in ("", None) for row in rows),
339
+ "elapsed_seconds": round(time.perf_counter() - started, 3),
340
+ }
341
+ csv_path, json_path = write_result_files(model_id, result, rows, metadata)
342
+
343
+ return (
344
+ table_values(rows),
345
+ metadata,
346
+ plot_delta_rows(rows),
347
+ csv_path,
348
+ json_path,
349
+ )
350
+
351
+
352
+ def initial_model(request: gr.Request):
353
+ if request is None:
354
+ return DEFAULT_MODEL_LABEL
355
+
356
+ query_params = getattr(request, "query_params", None)
357
+ model_id = None
358
+ if query_params is not None:
359
+ model_id = query_params.get("model")
360
+ if not model_id and getattr(request, "url", None):
361
+ parsed = parse_qs(urlparse(str(request.url)).query)
362
+ model_values = parsed.get("model")
363
+ model_id = model_values[0] if model_values else None
364
+
365
+ return MODEL_LABELS.get(model_id, DEFAULT_MODEL_LABEL)
366
+
367
+
368
+ with gr.Blocks(title="Regulatory Variant Effect") as demo:
369
+ gr.Markdown(
370
+ "# Regulatory Variant Effect\n"
371
+ "Score matched reference and alternative DNA windows with MultiMolecule regulatory variant-effect models."
372
+ )
373
+
374
+ model = gr.Dropdown(
375
+ choices=list(MODEL_OPTIONS.keys()),
376
+ value=DEFAULT_MODEL_LABEL,
377
+ label="Checkpoint",
378
+ )
379
+
380
+ with gr.Row():
381
+ reference_sequence = gr.Textbox(label="Reference DNA sequence", value=DEFAULT_REFERENCE_SEQUENCE, lines=5)
382
+ alternative_sequence = gr.Textbox(label="Alternative DNA sequence", value=DEFAULT_ALTERNATIVE_SEQUENCE, lines=5)
383
+
384
+ with gr.Accordion("Optional numeric features", open=False), gr.Row():
385
+ reference_features = gr.Textbox(
386
+ label="Reference features JSON/text",
387
+ placeholder='[0.1, 0.2, 0.3] or {"features": [0.1, 0.2, 0.3]}',
388
+ lines=3,
389
+ )
390
+ alternative_features = gr.Textbox(
391
+ label="Alternative features JSON/text",
392
+ placeholder="Leave blank to reuse reference features when provided.",
393
+ lines=3,
394
+ )
395
+
396
+ run = gr.Button("Run prediction", variant="primary")
397
+
398
+ delta_table = gr.Dataframe(headers=TABLE_HEADERS, label="Delta scores", interactive=False, wrap=True)
399
+ with gr.Row():
400
+ metadata = gr.JSON(label="Run metadata")
401
+ delta_plot = gr.Plot(label="Delta plot")
402
+
403
+ with gr.Row():
404
+ csv_download = gr.File(label="Download CSV")
405
+ json_download = gr.File(label="Download JSON")
406
+
407
+ run.click(
408
+ predict,
409
+ inputs=[model, reference_sequence, alternative_sequence, reference_features, alternative_features],
410
+ outputs=[delta_table, metadata, delta_plot, csv_download, json_download],
411
+ )
412
+ demo.load(initial_model, outputs=model)
413
+
414
+
415
+ if __name__ == "__main__":
416
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ matplotlib
2
+ multimolecule @ git+https://github.com/DLS5-Omics/multimolecule.git@master
3
+ numpy
4
+ torch
5
+ transformers