Spaces:
Sleeping
Sleeping
Deploy mRNA Design Studio (Docker SDK)
Browse filesThis view is limited to 50 files because it contains too many changes. Β See raw diff
- .dockerignore +13 -0
- .env.example +22 -0
- .gitignore +55 -0
- Dockerfile +42 -0
- MODELS_ADDED.md +177 -0
- Makefile +155 -0
- Procfile +1 -0
- README.md +54 -5
- core/__init__.py +0 -0
- core/analysis/__init__.py +0 -0
- core/analysis/analyzer.py +276 -0
- core/analysis/cai.py +187 -0
- core/analysis/dinucleotide.py +74 -0
- core/analysis/gc_content.py +68 -0
- core/analysis/homopolymers.py +84 -0
- core/analysis/kozak.py +138 -0
- core/analysis/restriction_sites.py +174 -0
- core/analysis/structure.py +83 -0
- core/analysis/uridine.py +81 -0
- core/data/__init__.py +1 -0
- core/data/seed_backbones.py +80 -0
- core/data/seed_parts.py +197 -0
- core/database/__init__.py +42 -0
- core/database/base.py +204 -0
- core/database/csv_importer.py +97 -0
- core/database/postgres.py +81 -0
- core/database/sqlite.py +68 -0
- core/models/__init__.py +0 -0
- core/models/parts.py +173 -0
- core/models/plasmid.py +178 -0
- core/models/sequence.py +196 -0
- core/models/worklist.py +159 -0
- core/optimization/__init__.py +0 -0
- core/sequence_tools/__init__.py +0 -0
- core/sequence_tools/clean_for_cloning.py +172 -0
- core/sequence_tools/codon_optimizer.py +155 -0
- demo/README.md +87 -0
- demo/cds_only_sequences.csv +5 -0
- demo/create_csv.py +179 -0
- demo/create_demo_db.py +260 -0
- demo/demo_models.py +166 -0
- demo/docker-compose.yml +19 -0
- demo/init_postgres.sql +89 -0
- demo/init_railway_db.py +73 -0
- demo/mrna_sequences.csv +3 -0
- demo/utr_library.csv +7 -0
- models/README.md +162 -0
- models/__init__.py +32 -0
- models/base.py +447 -0
- models/catalog.py +314 -0
.dockerignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
venv/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.pyc
|
| 5 |
+
.git/
|
| 6 |
+
.claude/
|
| 7 |
+
.env
|
| 8 |
+
*.egg-info/
|
| 9 |
+
dist/
|
| 10 |
+
build/
|
| 11 |
+
.pytest_cache/
|
| 12 |
+
.ruff_cache/
|
| 13 |
+
demo/mrna_parts.db
|
.env.example
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
# mRNA Design Studio β environment configuration
|
| 3 |
+
#
|
| 4 |
+
# Copy this file to .env and fill in the values you need.
|
| 5 |
+
# The app loads .env automatically via python-dotenv.
|
| 6 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
+
|
| 8 |
+
# ββ Password protection (pick ONE) ββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
+
|
| 10 |
+
# Option A: single shared password (any username accepted at login)
|
| 11 |
+
MRNA_STUDIO_PASSWORD=changeme
|
| 12 |
+
|
| 13 |
+
# Option B: multiple users (JSON β overrides MRNA_STUDIO_PASSWORD if set)
|
| 14 |
+
# MRNA_STUDIO_USERS={"alice": "pass1", "bob": "pass2"}
|
| 15 |
+
|
| 16 |
+
# ββ Session cookie secret (change in production!) βββββββββββββββββββββββββββ
|
| 17 |
+
MRNA_STUDIO_COOKIE_SECRET=replace-with-a-random-string
|
| 18 |
+
|
| 19 |
+
# ββ Server βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
+
# PORT=5007 # overridden by Railway / Render automatically
|
| 21 |
+
# HOST=0.0.0.0 # bind to all interfaces (required in containers)
|
| 22 |
+
# MRNA_STUDIO_RELOAD=1 # set to 1 to enable hot-reload during development
|
.gitignore
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
*.egg-info/
|
| 8 |
+
dist/
|
| 9 |
+
build/
|
| 10 |
+
*.egg
|
| 11 |
+
|
| 12 |
+
# Virtual Environment
|
| 13 |
+
.venv/
|
| 14 |
+
venv/
|
| 15 |
+
ENV/
|
| 16 |
+
env/
|
| 17 |
+
|
| 18 |
+
# IDE
|
| 19 |
+
.vscode/
|
| 20 |
+
.idea/
|
| 21 |
+
*.swp
|
| 22 |
+
*.swo
|
| 23 |
+
*~
|
| 24 |
+
|
| 25 |
+
# OS
|
| 26 |
+
.DS_Store
|
| 27 |
+
Thumbs.db
|
| 28 |
+
|
| 29 |
+
# Testing
|
| 30 |
+
.pytest_cache/
|
| 31 |
+
.coverage
|
| 32 |
+
htmlcov/
|
| 33 |
+
.tox/
|
| 34 |
+
|
| 35 |
+
# Panel/Bokeh
|
| 36 |
+
*.html
|
| 37 |
+
!docs/**/*.html
|
| 38 |
+
|
| 39 |
+
# Logs
|
| 40 |
+
*.log
|
| 41 |
+
|
| 42 |
+
# Database
|
| 43 |
+
*.db
|
| 44 |
+
*.sqlite
|
| 45 |
+
*.sqlite3
|
| 46 |
+
|
| 47 |
+
# Temporary files
|
| 48 |
+
*.tmp
|
| 49 |
+
.cache/
|
| 50 |
+
|
| 51 |
+
# Export files
|
| 52 |
+
worklist_export.csv
|
| 53 |
+
|
| 54 |
+
# Environment / secrets
|
| 55 |
+
.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
# mRNA Design Studio β production container
|
| 3 |
+
#
|
| 4 |
+
# Build: docker build -t mrna-studio .
|
| 5 |
+
# Run: docker run -p 5007:5007 -e MRNA_STUDIO_PASSWORD=changeme mrna-studio
|
| 6 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
+
FROM python:3.13-slim AS base
|
| 8 |
+
|
| 9 |
+
# Prevent Python from buffering stdout/stderr (makes logs visible immediately)
|
| 10 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 11 |
+
PYTHONUNBUFFERED=1
|
| 12 |
+
|
| 13 |
+
WORKDIR /app
|
| 14 |
+
|
| 15 |
+
# ββ Install system deps (needed by some bioinformatics packages) βββββββββββββ
|
| 16 |
+
RUN apt-get update && \
|
| 17 |
+
apt-get install -y --no-install-recommends gcc g++ libpq-dev && \
|
| 18 |
+
rm -rf /var/lib/apt/lists/*
|
| 19 |
+
|
| 20 |
+
# ββ Install Python deps (cached layer β only rebuilds when requirements change)
|
| 21 |
+
COPY requirements.txt .
|
| 22 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 23 |
+
|
| 24 |
+
# ββ Copy application code ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
+
COPY . .
|
| 26 |
+
|
| 27 |
+
# ββ Runtime ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
# Railway, Render, Fly etc. set $PORT automatically
|
| 29 |
+
ENV PORT=5007 \
|
| 30 |
+
HOST=0.0.0.0
|
| 31 |
+
|
| 32 |
+
# Hugging Face Spaces runs the container as UID 1000 (non-root). Point HOME and
|
| 33 |
+
# library cache dirs at /tmp (world-writable) so Panel/Bokeh/matplotlib can
|
| 34 |
+
# write caches without permission errors.
|
| 35 |
+
ENV HOME=/tmp \
|
| 36 |
+
XDG_CACHE_HOME=/tmp/.cache \
|
| 37 |
+
MPLCONFIGDIR=/tmp/matplotlib \
|
| 38 |
+
NUMBA_CACHE_DIR=/tmp/numba
|
| 39 |
+
|
| 40 |
+
EXPOSE ${PORT}
|
| 41 |
+
|
| 42 |
+
CMD ["python", "-m", "ui.app"]
|
MODELS_ADDED.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mRNA Scoring Models - Implementation Summary
|
| 2 |
+
|
| 3 |
+
I've successfully added two mRNA scoring models to the core package as requested.
|
| 4 |
+
|
| 5 |
+
## Models Added
|
| 6 |
+
|
| 7 |
+
### 1. **RNAstructure MFE Scorer** (`models/rna_structure_scorer.py`)
|
| 8 |
+
|
| 9 |
+
**What it does**: Predicts the minimum free energy (MFE) of mRNA secondary structure to assess translation efficiency.
|
| 10 |
+
|
| 11 |
+
**Key features**:
|
| 12 |
+
- Uses ViennaRNA when available for accurate MFE calculation
|
| 13 |
+
- Falls back to GC-content based proxy scoring when ViennaRNA is not installed
|
| 14 |
+
- Score range: 0-100 (optimal: 40-70)
|
| 15 |
+
- Higher scores indicate stronger secondary structures
|
| 16 |
+
|
| 17 |
+
**Scientific basis**: Based on ViennaRNA thermodynamic calculations (Lorenz et al., 2011)
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
### 2. **mRNA Stability Scorer** (`models/mrna_stability_scorer.py`)
|
| 22 |
+
|
| 23 |
+
**What it does**: Composite stability prediction combining five established mRNA design principles.
|
| 24 |
+
|
| 25 |
+
**Scoring components**:
|
| 26 |
+
1. **GC Content** (30% weight) - Optimal range: 50-60%
|
| 27 |
+
2. **Codon Adaptation Index** (25% weight) - Codon optimization
|
| 28 |
+
3. **Homopolymer Detection** (20% weight) - Penalizes long identical runs
|
| 29 |
+
4. **5' UTR Structure** (15% weight) - Moderate stability preferred
|
| 30 |
+
5. **Kozak Consensus** (10% weight) - Translation initiation strength
|
| 31 |
+
|
| 32 |
+
**Key features**:
|
| 33 |
+
- Score range: 0-100 (70+ = excellent, 40-70 = acceptable, <40 = poor)
|
| 34 |
+
- Configurable for different organisms (default: human)
|
| 35 |
+
- Individual component scores accessible for detailed analysis
|
| 36 |
+
|
| 37 |
+
**Scientific basis**:
|
| 38 |
+
- Kozak sequence analysis (Mauro & Edelman, 2002)
|
| 39 |
+
- CAI methodology (Sharp & Li, 1987)
|
| 40 |
+
- mRNA stability research (Presnyak et al., 2015)
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## Files Created
|
| 45 |
+
|
| 46 |
+
```
|
| 47 |
+
models/
|
| 48 |
+
βββ rna_structure_scorer.py # RNAstructure MFE model
|
| 49 |
+
βββ mrna_stability_scorer.py # mRNA Stability composite model
|
| 50 |
+
βββ __init__.py # Updated to export new models
|
| 51 |
+
βββ README.md # Full documentation
|
| 52 |
+
|
| 53 |
+
tests/
|
| 54 |
+
βββ test_models.py # Added comprehensive tests for both models
|
| 55 |
+
|
| 56 |
+
demo/
|
| 57 |
+
βββ demo_models.py # Demo script showing usage
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## Testing Results
|
| 63 |
+
|
| 64 |
+
All tests pass successfully:
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
$ pytest tests/test_models.py::TestRNAStructureMFEScorer -v
|
| 68 |
+
$ pytest tests/test_models.py::TestmRNAStabilityScorer -v
|
| 69 |
+
|
| 70 |
+
8 passed in 0.13s β
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
## Usage Example
|
| 76 |
+
|
| 77 |
+
```python
|
| 78 |
+
from core.models.sequence import mRNASequence
|
| 79 |
+
from models import RNAStructureMFEScorer, mRNAStabilityScorer
|
| 80 |
+
|
| 81 |
+
# Create a sequence
|
| 82 |
+
seq = mRNASequence(
|
| 83 |
+
name="my_mrna",
|
| 84 |
+
source="local",
|
| 85 |
+
five_prime_utr="GTTGCTCCTTCGGGCCTGTGGCGGCT",
|
| 86 |
+
kozak="GCCACCATGG",
|
| 87 |
+
cds="ATGGTGAGCAAGGGCGAGGAG...",
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
# Score with MFE model
|
| 91 |
+
mfe_scorer = RNAStructureMFEScorer()
|
| 92 |
+
mfe_score = mfe_scorer.score(seq)
|
| 93 |
+
print(f"MFE Score: {mfe_score:.1f}/100")
|
| 94 |
+
|
| 95 |
+
# Score with Stability model
|
| 96 |
+
stability_scorer = mRNAStabilityScorer(organism="human")
|
| 97 |
+
stability_score = stability_scorer.score(seq)
|
| 98 |
+
print(f"Stability Score: {stability_score:.1f}/100")
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## Demo Output
|
| 104 |
+
|
| 105 |
+
Run the demo to see both models in action:
|
| 106 |
+
|
| 107 |
+
```bash
|
| 108 |
+
$ PYTHONPATH=. .venv/bin/python demo/demo_models.py
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
Sample output:
|
| 112 |
+
```
|
| 113 |
+
RNAstructure MFE Scorer
|
| 114 |
+
Score: 64.2/100
|
| 115 |
+
Interpretation: Optimal structure for translation β
|
| 116 |
+
|
| 117 |
+
mRNA Stability Scorer
|
| 118 |
+
Overall Score: 76.2/100
|
| 119 |
+
Interpretation: Excellent design β
|
| 120 |
+
|
| 121 |
+
Component Breakdown:
|
| 122 |
+
GC Content (30%): 100.0/100
|
| 123 |
+
CAI (25%): 63.9/100
|
| 124 |
+
Homopolymers (20%): 65.0/100
|
| 125 |
+
5' UTR (15%): 61.5/100
|
| 126 |
+
Kozak (10%): 80.0/100
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
---
|
| 130 |
+
|
| 131 |
+
## ModelRegistry Integration
|
| 132 |
+
|
| 133 |
+
Both models are compatible with the existing ModelRegistry system:
|
| 134 |
+
|
| 135 |
+
```python
|
| 136 |
+
from models import ModelRegistry
|
| 137 |
+
|
| 138 |
+
registry = ModelRegistry()
|
| 139 |
+
registry._register(RNAStructureMFEScorer(), "scoring", "builtin", "")
|
| 140 |
+
registry._register(mRNAStabilityScorer(), "scoring", "builtin", "")
|
| 141 |
+
|
| 142 |
+
# Batch score sequences
|
| 143 |
+
results = registry.run_scoring("mRNA Stability", sequences)
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
## Dependencies
|
| 149 |
+
|
| 150 |
+
**Required**:
|
| 151 |
+
- Core Python libraries (no additional dependencies for basic functionality)
|
| 152 |
+
|
| 153 |
+
**Optional** (for enhanced features):
|
| 154 |
+
- `ViennaRNA` - For accurate RNA secondary structure prediction
|
| 155 |
+
- `BioPython` - For advanced codon usage analysis
|
| 156 |
+
|
| 157 |
+
Both models degrade gracefully when optional dependencies are missing.
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## Next Steps
|
| 162 |
+
|
| 163 |
+
To integrate these models into the UI sidebar:
|
| 164 |
+
|
| 165 |
+
1. Update `ui/components/sidebar.py` to show loaded models
|
| 166 |
+
2. Implement model loading UI in the "β Load Model" button handler
|
| 167 |
+
3. Auto-register built-in models on app startup
|
| 168 |
+
4. Add model scoring to the Worklist view
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
## References
|
| 173 |
+
|
| 174 |
+
- **ViennaRNA**: Lorenz et al. (2011). Algorithms for Molecular Biology, 6:26
|
| 175 |
+
- **Kozak**: Mauro & Edelman (2002). PNAS, 99(19):12031-12036
|
| 176 |
+
- **CAI**: Sharp & Li (1987). Nucleic Acids Research, 15(3):1281-1295
|
| 177 |
+
- **mRNA Stability**: Presnyak et al. (2015). Cell, 160(6):1111-1124
|
Makefile
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
# mRNA Design Studio β Makefile
|
| 3 |
+
#
|
| 4 |
+
# Quick start (from scratch, no Python required):
|
| 5 |
+
# make setup # one-time: installs uv, Python 3.13, and all deps
|
| 6 |
+
# make run # start the app β http://localhost:5007
|
| 7 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 8 |
+
.PHONY: help setup run run-debug run-secure kill restart clean test lint \
|
| 9 |
+
db-up db-down db-status docker-build docker-run
|
| 10 |
+
|
| 11 |
+
# ββ OS detection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12 |
+
# $(OS) is set to "Windows_NT" on Windows by the shell; empty on macOS/Linux.
|
| 13 |
+
ifeq ($(OS),Windows_NT)
|
| 14 |
+
VENV_BIN := .venv/Scripts
|
| 15 |
+
else
|
| 16 |
+
VENV_BIN := .venv/bin
|
| 17 |
+
endif
|
| 18 |
+
|
| 19 |
+
PYTHON := $(VENV_BIN)/python
|
| 20 |
+
PORT := 5007
|
| 21 |
+
URL := http://localhost:$(PORT)
|
| 22 |
+
COMPOSE := docker compose -f demo/docker-compose.yml
|
| 23 |
+
|
| 24 |
+
# Where uv installs itself by default (used right after first install,
|
| 25 |
+
# before the user's shell has reloaded PATH).
|
| 26 |
+
UV_HOME := $(or $(XDG_BIN_HOME),$(HOME)/.local/bin)
|
| 27 |
+
UV := $(shell command -v uv 2>/dev/null || echo "$(UV_HOME)/uv")
|
| 28 |
+
|
| 29 |
+
# ββ Default target ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
help: ## Show available commands
|
| 31 |
+
@echo ""
|
| 32 |
+
@echo " mRNA Design Studio"
|
| 33 |
+
@echo " ===================="
|
| 34 |
+
@echo ""
|
| 35 |
+
@echo " First time? Run: make setup"
|
| 36 |
+
@echo " Then: make run β $(URL)"
|
| 37 |
+
@echo ""
|
| 38 |
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
| 39 |
+
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
|
| 40 |
+
@echo ""
|
| 41 |
+
|
| 42 |
+
# ββ Setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
+
setup: _ensure-uv _ensure-venv _install-deps ## One-time setup: install Python, create venv, install deps
|
| 44 |
+
@echo ""
|
| 45 |
+
@echo " Setup complete!"
|
| 46 |
+
@echo " Run 'make run' to start the app."
|
| 47 |
+
@echo ""
|
| 48 |
+
|
| 49 |
+
_ensure-uv:
|
| 50 |
+
@command -v uv >/dev/null 2>&1 && exit 0; \
|
| 51 |
+
if [ -x "$(UV_HOME)/uv" ]; then exit 0; fi; \
|
| 52 |
+
echo "Installing uv (fast Python package manager)..."; \
|
| 53 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null \
|
| 54 |
+
|| { echo "curl failed β trying wget..."; \
|
| 55 |
+
wget -qO- https://astral.sh/uv/install.sh | sh; }; \
|
| 56 |
+
if ! command -v uv >/dev/null 2>&1 && [ ! -x "$(UV_HOME)/uv" ]; then \
|
| 57 |
+
echo ""; \
|
| 58 |
+
echo " ERROR: uv installed but not found in PATH."; \
|
| 59 |
+
echo " Restart your terminal, then re-run: make setup"; \
|
| 60 |
+
echo ""; \
|
| 61 |
+
exit 1; \
|
| 62 |
+
fi
|
| 63 |
+
|
| 64 |
+
_ensure-venv:
|
| 65 |
+
@if [ ! -d ".venv" ]; then \
|
| 66 |
+
echo "Creating virtual environment..."; \
|
| 67 |
+
if command -v $(UV) >/dev/null 2>&1; then \
|
| 68 |
+
$(UV) venv .venv --python 3.13; \
|
| 69 |
+
else \
|
| 70 |
+
python3 -m venv .venv; \
|
| 71 |
+
fi; \
|
| 72 |
+
fi
|
| 73 |
+
|
| 74 |
+
_install-deps:
|
| 75 |
+
@echo "Installing dependencies..."
|
| 76 |
+
@if command -v $(UV) >/dev/null 2>&1; then \
|
| 77 |
+
$(UV) pip install -r requirements-dev.txt --python $(PYTHON) --quiet; \
|
| 78 |
+
else \
|
| 79 |
+
$(PYTHON) -m pip install -r requirements-dev.txt --quiet; \
|
| 80 |
+
fi
|
| 81 |
+
|
| 82 |
+
# ββ App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 83 |
+
run: _check-venv ## Start the app β http://localhost:5007
|
| 84 |
+
$(PYTHON) -m ui.app
|
| 85 |
+
|
| 86 |
+
run-secure: _check-venv ## Start with password protection (.env must exist)
|
| 87 |
+
@if [ ! -f .env ]; then \
|
| 88 |
+
echo ""; \
|
| 89 |
+
echo " No .env file found. Copy the template first:"; \
|
| 90 |
+
echo " cp .env.example .env"; \
|
| 91 |
+
echo " Then set MRNA_STUDIO_PASSWORD in .env"; \
|
| 92 |
+
echo ""; \
|
| 93 |
+
exit 1; \
|
| 94 |
+
fi
|
| 95 |
+
MRNA_STUDIO_RELOAD=1 $(PYTHON) -m ui.app
|
| 96 |
+
|
| 97 |
+
run-debug: _check-venv ## Start with DEBUG-level logging
|
| 98 |
+
LOG_LEVEL=DEBUG $(PYTHON) -c "\
|
| 99 |
+
import logging, sys, os; \
|
| 100 |
+
logging.basicConfig(level=getattr(logging, os.environ.get('LOG_LEVEL','INFO')), \
|
| 101 |
+
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%H:%M:%S', stream=sys.stderr); \
|
| 102 |
+
from ui.app import main; main()"
|
| 103 |
+
|
| 104 |
+
kill: ## Stop the app if it's running
|
| 105 |
+
ifeq ($(OS),Windows_NT)
|
| 106 |
+
@powershell -Command \
|
| 107 |
+
"Get-NetTCPConnection -LocalPort $(PORT) -ErrorAction SilentlyContinue \
|
| 108 |
+
| ForEach-Object { Stop-Process -Id $$_.OwningProcess -Force }" 2>/dev/null \
|
| 109 |
+
|| echo "Nothing running on port $(PORT)"
|
| 110 |
+
else
|
| 111 |
+
@lsof -ti:$(PORT) | xargs kill -9 2>/dev/null || echo "Nothing running on port $(PORT)"
|
| 112 |
+
endif
|
| 113 |
+
|
| 114 |
+
restart: kill run ## Kill + restart the app
|
| 115 |
+
|
| 116 |
+
# ββ Dev tools βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 117 |
+
test: _check-venv ## Run the test suite
|
| 118 |
+
$(PYTHON) -m pytest tests/ -v
|
| 119 |
+
|
| 120 |
+
lint: _check-venv ## Lint with ruff
|
| 121 |
+
$(PYTHON) -m ruff check .
|
| 122 |
+
|
| 123 |
+
clean: ## Remove the virtual environment
|
| 124 |
+
rm -rf .venv
|
| 125 |
+
@echo "Removed .venv β run 'make setup' to recreate."
|
| 126 |
+
|
| 127 |
+
# ββ Database ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 128 |
+
db-up: ## Start the demo PostgreSQL database
|
| 129 |
+
$(COMPOSE) up -d
|
| 130 |
+
@echo "Waiting for PostgreSQL to be ready..."
|
| 131 |
+
@until docker exec mrna_studio_demo_db pg_isready -U demo_user -d mrna_studio >/dev/null 2>&1; do sleep 1; done
|
| 132 |
+
@echo "PostgreSQL is ready on localhost:5432"
|
| 133 |
+
|
| 134 |
+
db-down: ## Stop the demo database
|
| 135 |
+
$(COMPOSE) down
|
| 136 |
+
|
| 137 |
+
db-status: ## Check database container status
|
| 138 |
+
@docker ps --filter name=mrna_studio_demo_db --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || echo "Container not running"
|
| 139 |
+
|
| 140 |
+
# ββ Docker (production) ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 141 |
+
docker-build: ## Build production Docker image
|
| 142 |
+
docker build -t mrna-studio .
|
| 143 |
+
|
| 144 |
+
docker-run: ## Run container with .env file (password-protected)
|
| 145 |
+
docker run --rm -p 5007:5007 --env-file .env mrna-studio
|
| 146 |
+
|
| 147 |
+
# ββ Internal helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 148 |
+
_check-venv:
|
| 149 |
+
@if [ ! -d ".venv" ]; then \
|
| 150 |
+
echo ""; \
|
| 151 |
+
echo " No virtual environment found."; \
|
| 152 |
+
echo " Run 'make setup' first."; \
|
| 153 |
+
echo ""; \
|
| 154 |
+
exit 1; \
|
| 155 |
+
fi
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: python -m ui.app
|
README.md
CHANGED
|
@@ -1,10 +1,59 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: mRNA Design Studio
|
| 3 |
+
emoji: π§¬
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 5007
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# mRNA Design Studio
|
| 12 |
+
|
| 13 |
+
A browser-based workbench for designing, analyzing, and assembling mRNA sequences. Import sequence data from CSV files or PostgreSQL databases, run codon-usage and structural analyses, score sequences with pluggable models, assemble inserts into plasmid backbones with QC checks, and export results β all from a single reactive UI powered by Panel.
|
| 14 |
+
|
| 15 |
+
- **Import Data** β load sequences from CSV/Excel files or PostgreSQL, map columns to the mRNA model, and organize into worklists
|
| 16 |
+
- **Model Repository** β register local Python scoring/generative models or remote API endpoints
|
| 17 |
+
- **Worklist** β inspect, analyze (GC%, CAI, homopolymers, restriction sites), score with loaded models, and export sequences as CSV
|
| 18 |
+
- **Parts Workshop** β browse and compose reusable sequence parts (UTRs, Kozak, CDS, PolyA)
|
| 19 |
+
- **Assemble Plasmid** β pick a backbone and cloning strategy, run QC, and export assembled constructs
|
| 20 |
+
- **Generate Sequences** β create optimized mRNA variants with configurable generation settings
|
| 21 |
+
|
| 22 |
+
## Getting Started
|
| 23 |
+
|
| 24 |
+
The only prerequisite is a terminal with `make` and `curl` (both come pre-installed on macOS; on Windows use Git Bash or WSL). No Python install is required β the setup command handles everything.
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
git clone <repo-url> && cd mrna_design_studio
|
| 28 |
+
|
| 29 |
+
make setup # installs uv, Python 3.13, and all dependencies (~1 min)
|
| 30 |
+
make run # starts the app at http://localhost:5007
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
Run `make` by itself to see every available command:
|
| 34 |
+
|
| 35 |
+
```
|
| 36 |
+
make help
|
| 37 |
+
|
| 38 |
+
setup One-time setup: install Python, create venv, install deps
|
| 39 |
+
run Start the app
|
| 40 |
+
run-debug Start with DEBUG-level logging
|
| 41 |
+
kill Stop the app if it's running
|
| 42 |
+
restart Kill + restart the app
|
| 43 |
+
test Run the test suite
|
| 44 |
+
lint Lint with ruff
|
| 45 |
+
clean Remove the virtual environment
|
| 46 |
+
db-up Start the demo PostgreSQL database (requires Docker)
|
| 47 |
+
db-down Stop the demo database
|
| 48 |
+
db-status Check database container status
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### Demo database (optional)
|
| 52 |
+
|
| 53 |
+
A Docker Compose file is included to spin up a PostgreSQL instance pre-loaded with sample mRNA sequences:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
make db-up # start the container
|
| 57 |
+
make run # connect using the pre-filled credentials on the Import Data tab
|
| 58 |
+
make db-down # stop when done
|
| 59 |
+
```
|
core/__init__.py
ADDED
|
File without changes
|
core/analysis/__init__.py
ADDED
|
File without changes
|
core/analysis/analyzer.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SequenceAnalyzer β main analysis entry point.
|
| 3 |
+
|
| 4 |
+
Runs all analysis modules against an mRNASequence and returns a
|
| 5 |
+
structured AnalysisReport. Results are cached on the sequence object
|
| 6 |
+
so repeated calls are cheap.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from typing import Any, Dict, List, Optional
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
|
| 15 |
+
from core.models.sequence import mRNASequence
|
| 16 |
+
from core.analysis.gc_content import (
|
| 17 |
+
gc_percent,
|
| 18 |
+
gc_sliding_window,
|
| 19 |
+
gc_by_codon_position,
|
| 20 |
+
)
|
| 21 |
+
from core.analysis.cai import calculate_cai, codon_usage_report, CODON_TABLES
|
| 22 |
+
from core.analysis.homopolymers import detect_homopolymers, HomopolymerRun
|
| 23 |
+
from core.analysis.restriction_sites import (
|
| 24 |
+
scan_restriction_sites,
|
| 25 |
+
RestrictionSiteHit,
|
| 26 |
+
COMMON_ENZYMES,
|
| 27 |
+
)
|
| 28 |
+
from core.analysis.kozak import check_kozak, KozakResult
|
| 29 |
+
from core.analysis.structure import predict_structure, StructureResult
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class AnalysisReport:
|
| 34 |
+
"""All analysis results for a single mRNASequence."""
|
| 35 |
+
sequence_id: str
|
| 36 |
+
sequence_name: str
|
| 37 |
+
sequence_length: int
|
| 38 |
+
|
| 39 |
+
# GC content
|
| 40 |
+
gc_percent_global: float = 0.0
|
| 41 |
+
gc_sliding_positions: Optional[np.ndarray] = None # centre positions
|
| 42 |
+
gc_sliding_values: Optional[np.ndarray] = None # GC% per window
|
| 43 |
+
gc_by_codon_position: Optional[Dict[str, float]] = None
|
| 44 |
+
|
| 45 |
+
# Codon Adaptation Index
|
| 46 |
+
cai: Optional[float] = None
|
| 47 |
+
cai_organism: Optional[str] = None
|
| 48 |
+
codon_usage: Optional[Dict[str, int]] = None
|
| 49 |
+
|
| 50 |
+
# Homopolymers
|
| 51 |
+
homopolymer_runs: List[HomopolymerRun] = field(default_factory=list)
|
| 52 |
+
homopolymer_count: int = 0
|
| 53 |
+
longest_homopolymer: int = 0
|
| 54 |
+
|
| 55 |
+
# Restriction sites
|
| 56 |
+
restriction_hits: Dict[str, List[RestrictionSiteHit]] = field(default_factory=dict)
|
| 57 |
+
restriction_enzymes_present: List[str] = field(default_factory=list)
|
| 58 |
+
|
| 59 |
+
# Start / stop codon validation
|
| 60 |
+
has_start_codon: Optional[bool] = None
|
| 61 |
+
has_stop_codon: Optional[bool] = None
|
| 62 |
+
stop_codon: Optional[str] = None
|
| 63 |
+
in_frame: Optional[bool] = None
|
| 64 |
+
|
| 65 |
+
# Kozak context
|
| 66 |
+
kozak: Optional[KozakResult] = None
|
| 67 |
+
|
| 68 |
+
# Secondary structure (ViennaRNA)
|
| 69 |
+
structure: Optional[StructureResult] = None
|
| 70 |
+
|
| 71 |
+
# Errors / warnings generated during analysis
|
| 72 |
+
warnings: List[str] = field(default_factory=list)
|
| 73 |
+
|
| 74 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 75 |
+
return {
|
| 76 |
+
"sequence_id": self.sequence_id,
|
| 77 |
+
"sequence_name": self.sequence_name,
|
| 78 |
+
"sequence_length": self.sequence_length,
|
| 79 |
+
"gc_content": self.gc_percent_global,
|
| 80 |
+
"gc_percent_global": self.gc_percent_global,
|
| 81 |
+
"gc_by_codon_position": self.gc_by_codon_position,
|
| 82 |
+
"cai": self.cai,
|
| 83 |
+
"cai_organism": self.cai_organism,
|
| 84 |
+
"homopolymer_count": self.homopolymer_count,
|
| 85 |
+
"longest_homopolymer": self.longest_homopolymer,
|
| 86 |
+
"restriction_site_count": len(self.restriction_enzymes_present),
|
| 87 |
+
"restriction_enzymes_present": self.restriction_enzymes_present,
|
| 88 |
+
"has_start_codon": self.has_start_codon,
|
| 89 |
+
"has_stop_codon": self.has_stop_codon,
|
| 90 |
+
"stop_codon": self.stop_codon,
|
| 91 |
+
"in_frame": self.in_frame,
|
| 92 |
+
"kozak_score": self.kozak.score if self.kozak else None,
|
| 93 |
+
"kozak_strength": self.kozak.strength if self.kozak else None,
|
| 94 |
+
"mfe": self.structure.mfe if self.structure else None,
|
| 95 |
+
"warnings": self.warnings,
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class SequenceAnalyzer:
|
| 100 |
+
"""
|
| 101 |
+
Runs analysis modules against mRNASequence objects.
|
| 102 |
+
|
| 103 |
+
Results are cached inside the sequence's _analysis_cache dict
|
| 104 |
+
(keyed by analysis type) so re-running is a cache lookup.
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
def __init__(
|
| 108 |
+
self,
|
| 109 |
+
gc_window: int = 100,
|
| 110 |
+
gc_step: int = 1,
|
| 111 |
+
homopolymer_min_run: int = 5,
|
| 112 |
+
restriction_enzymes: Optional[List[str]] = None,
|
| 113 |
+
cai_organism: str = "human",
|
| 114 |
+
cai_custom_table: Optional[Dict[str, float]] = None,
|
| 115 |
+
) -> None:
|
| 116 |
+
self.gc_window = gc_window
|
| 117 |
+
self.gc_step = gc_step
|
| 118 |
+
self.homopolymer_min_run = homopolymer_min_run
|
| 119 |
+
self.restriction_enzymes = restriction_enzymes or list(COMMON_ENZYMES.keys())
|
| 120 |
+
self.cai_organism = cai_organism
|
| 121 |
+
self.cai_custom_table = cai_custom_table
|
| 122 |
+
|
| 123 |
+
# ββ Individual analysis methods βββββββββββββββββββββββββββββββββββββββββ
|
| 124 |
+
|
| 125 |
+
def analyze_gc(self, sequence: str) -> Dict[str, Any]:
|
| 126 |
+
positions, values = gc_sliding_window(sequence, self.gc_window, self.gc_step)
|
| 127 |
+
return {
|
| 128 |
+
"gc_percent_global": gc_percent(sequence),
|
| 129 |
+
"gc_sliding_positions": positions,
|
| 130 |
+
"gc_sliding_values": values,
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
def analyze_cai(self, cds: str) -> Dict[str, Any]:
|
| 134 |
+
try:
|
| 135 |
+
cai_value = calculate_cai(cds, self.cai_organism, self.cai_custom_table)
|
| 136 |
+
usage = codon_usage_report(cds)
|
| 137 |
+
return {
|
| 138 |
+
"cai": cai_value,
|
| 139 |
+
"cai_organism": self.cai_custom_table and "custom" or self.cai_organism,
|
| 140 |
+
"codon_usage": usage,
|
| 141 |
+
}
|
| 142 |
+
except Exception as e:
|
| 143 |
+
return {"cai": None, "cai_organism": None, "codon_usage": None, "error": str(e)}
|
| 144 |
+
|
| 145 |
+
def analyze_homopolymers(self, sequence: str) -> Dict[str, Any]:
|
| 146 |
+
runs = detect_homopolymers(sequence, self.homopolymer_min_run)
|
| 147 |
+
return {
|
| 148 |
+
"homopolymer_runs": runs,
|
| 149 |
+
"homopolymer_count": len(runs),
|
| 150 |
+
"longest_homopolymer": max((r.length for r in runs), default=0),
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
def analyze_restriction_sites(self, sequence: str) -> Dict[str, Any]:
|
| 154 |
+
hits = scan_restriction_sites(sequence, self.restriction_enzymes)
|
| 155 |
+
return {
|
| 156 |
+
"restriction_hits": hits,
|
| 157 |
+
"restriction_enzymes_present": list(hits.keys()),
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
def validate_cds(self, cds: str) -> Dict[str, Any]:
|
| 161 |
+
seq = cds.upper().replace("U", "T")
|
| 162 |
+
start_codons = {"ATG"}
|
| 163 |
+
stop_codons = {"TAA", "TAG", "TGA"}
|
| 164 |
+
has_start = seq[:3] in start_codons if len(seq) >= 3 else False
|
| 165 |
+
stop = seq[-3:] if len(seq) >= 3 else ""
|
| 166 |
+
has_stop = stop in stop_codons
|
| 167 |
+
in_frame = len(seq) % 3 == 0
|
| 168 |
+
return {
|
| 169 |
+
"has_start_codon": has_start,
|
| 170 |
+
"has_stop_codon": has_stop,
|
| 171 |
+
"stop_codon": stop if has_stop else None,
|
| 172 |
+
"in_frame": in_frame,
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
def analyze_kozak(self, sequence: str) -> Dict[str, Any]:
|
| 176 |
+
try:
|
| 177 |
+
result = check_kozak(sequence)
|
| 178 |
+
return {"kozak": result}
|
| 179 |
+
except ValueError as e:
|
| 180 |
+
return {"kozak": None, "kozak_warning": str(e)}
|
| 181 |
+
|
| 182 |
+
def analyze_structure(self, sequence: str) -> Dict[str, Any]:
|
| 183 |
+
result = predict_structure(sequence)
|
| 184 |
+
return {"structure": result}
|
| 185 |
+
|
| 186 |
+
# ββ Full report βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 187 |
+
|
| 188 |
+
def run_full_analysis(
|
| 189 |
+
self,
|
| 190 |
+
seq: mRNASequence,
|
| 191 |
+
include_structure: bool = True,
|
| 192 |
+
force_rerun: bool = False,
|
| 193 |
+
) -> AnalysisReport:
|
| 194 |
+
"""
|
| 195 |
+
Run all analysis modules against seq and return an AnalysisReport.
|
| 196 |
+
|
| 197 |
+
Results are cached in seq._analysis_cache. Set force_rerun=True
|
| 198 |
+
to bypass the cache.
|
| 199 |
+
"""
|
| 200 |
+
cache_key = "full_analysis"
|
| 201 |
+
if not force_rerun and cache_key in seq._analysis_cache:
|
| 202 |
+
return seq._analysis_cache[cache_key] # type: ignore[return-value]
|
| 203 |
+
|
| 204 |
+
full_seq = seq.assembled_sequence
|
| 205 |
+
warnings: List[str] = []
|
| 206 |
+
|
| 207 |
+
report = AnalysisReport(
|
| 208 |
+
sequence_id=seq.id,
|
| 209 |
+
sequence_name=seq.name,
|
| 210 |
+
sequence_length=len(full_seq),
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
# GC content β run on full sequence
|
| 214 |
+
gc_data = self.analyze_gc(full_seq)
|
| 215 |
+
report.gc_percent_global = gc_data["gc_percent_global"]
|
| 216 |
+
report.gc_sliding_positions = gc_data["gc_sliding_positions"]
|
| 217 |
+
report.gc_sliding_values = gc_data["gc_sliding_values"]
|
| 218 |
+
|
| 219 |
+
# GC by codon position β only if CDS available
|
| 220 |
+
if seq.cds and len(seq.cds) % 3 == 0:
|
| 221 |
+
try:
|
| 222 |
+
report.gc_by_codon_position = gc_by_codon_position(seq.cds)
|
| 223 |
+
except Exception as e:
|
| 224 |
+
warnings.append(f"GC by codon position failed: {e}")
|
| 225 |
+
|
| 226 |
+
# CAI β only on CDS
|
| 227 |
+
if seq.cds:
|
| 228 |
+
cai_data = self.analyze_cai(seq.cds)
|
| 229 |
+
report.cai = cai_data.get("cai")
|
| 230 |
+
report.cai_organism = cai_data.get("cai_organism")
|
| 231 |
+
report.codon_usage = cai_data.get("codon_usage")
|
| 232 |
+
if "error" in cai_data:
|
| 233 |
+
warnings.append(f"CAI error: {cai_data['error']}")
|
| 234 |
+
|
| 235 |
+
# CDS validation
|
| 236 |
+
cds_data = self.validate_cds(seq.cds)
|
| 237 |
+
report.has_start_codon = cds_data["has_start_codon"]
|
| 238 |
+
report.has_stop_codon = cds_data["has_stop_codon"]
|
| 239 |
+
report.stop_codon = cds_data["stop_codon"]
|
| 240 |
+
report.in_frame = cds_data["in_frame"]
|
| 241 |
+
|
| 242 |
+
if not report.has_start_codon:
|
| 243 |
+
warnings.append("CDS does not begin with ATG.")
|
| 244 |
+
if not report.has_stop_codon:
|
| 245 |
+
warnings.append("CDS does not end with a stop codon.")
|
| 246 |
+
if not report.in_frame:
|
| 247 |
+
warnings.append("CDS length is not divisible by 3.")
|
| 248 |
+
|
| 249 |
+
# Homopolymers β full assembled sequence
|
| 250 |
+
hp_data = self.analyze_homopolymers(full_seq)
|
| 251 |
+
report.homopolymer_runs = hp_data["homopolymer_runs"]
|
| 252 |
+
report.homopolymer_count = hp_data["homopolymer_count"]
|
| 253 |
+
report.longest_homopolymer = hp_data["longest_homopolymer"]
|
| 254 |
+
|
| 255 |
+
# Restriction sites
|
| 256 |
+
rs_data = self.analyze_restriction_sites(full_seq)
|
| 257 |
+
report.restriction_hits = rs_data["restriction_hits"]
|
| 258 |
+
report.restriction_enzymes_present = rs_data["restriction_enzymes_present"]
|
| 259 |
+
|
| 260 |
+
# Kozak β try on kozak component, then fall back to full sequence
|
| 261 |
+
kozak_seq = seq.kozak or full_seq
|
| 262 |
+
kozak_data = self.analyze_kozak(kozak_seq)
|
| 263 |
+
report.kozak = kozak_data.get("kozak")
|
| 264 |
+
if "kozak_warning" in kozak_data:
|
| 265 |
+
warnings.append(kozak_data["kozak_warning"])
|
| 266 |
+
|
| 267 |
+
# Secondary structure
|
| 268 |
+
if include_structure:
|
| 269 |
+
struct_data = self.analyze_structure(full_seq)
|
| 270 |
+
report.structure = struct_data["structure"]
|
| 271 |
+
|
| 272 |
+
report.warnings = warnings
|
| 273 |
+
|
| 274 |
+
# Cache result
|
| 275 |
+
seq._analysis_cache[cache_key] = report
|
| 276 |
+
return report
|
core/analysis/cai.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Codon Adaptation Index (CAI) calculation.
|
| 3 |
+
|
| 4 |
+
Uses BioPython's CodonAdaptationIndex and codon usage tables.
|
| 5 |
+
Supports human and a set of common lab organisms. Additional organisms
|
| 6 |
+
can be added by providing a codon usage table as a dict.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
from typing import Dict, Optional
|
| 12 |
+
|
| 13 |
+
# Codon usage tables: {codon: relative_adaptiveness}
|
| 14 |
+
# Tables below are RSCU (relative synonymous codon usage) normalized per
|
| 15 |
+
# synonymous family to relative adaptiveness (0β1). Human table derived
|
| 16 |
+
# from Homo sapiens Kazusa database (high-expression genes).
|
| 17 |
+
|
| 18 |
+
_HUMAN_RSCU: Dict[str, float] = {
|
| 19 |
+
# Phe
|
| 20 |
+
"TTT": 0.55, "TTC": 1.00,
|
| 21 |
+
# Leu
|
| 22 |
+
"TTA": 0.07, "TTG": 0.19, "CTT": 0.42, "CTC": 0.68, "CTA": 0.16, "CTG": 1.00,
|
| 23 |
+
# Ile
|
| 24 |
+
"ATT": 0.71, "ATC": 1.00, "ATA": 0.31,
|
| 25 |
+
# Met
|
| 26 |
+
"ATG": 1.00,
|
| 27 |
+
# Val
|
| 28 |
+
"GTT": 0.46, "GTC": 0.62, "GTA": 0.27, "GTG": 1.00,
|
| 29 |
+
# Ser
|
| 30 |
+
"TCT": 0.85, "TCC": 1.00, "TCA": 0.69, "TCG": 0.27, "AGT": 0.72, "AGC": 0.97,
|
| 31 |
+
# Pro
|
| 32 |
+
"CCT": 0.85, "CCC": 1.00, "CCA": 0.83, "CCG": 0.22,
|
| 33 |
+
# Thr
|
| 34 |
+
"ACT": 0.74, "ACC": 1.00, "ACA": 0.77, "ACG": 0.27,
|
| 35 |
+
# Ala
|
| 36 |
+
"GCT": 0.91, "GCC": 1.00, "GCA": 0.67, "GCG": 0.19,
|
| 37 |
+
# Tyr
|
| 38 |
+
"TAT": 0.57, "TAC": 1.00,
|
| 39 |
+
# Stop
|
| 40 |
+
"TAA": 1.00, "TAG": 0.22, "TGA": 0.61,
|
| 41 |
+
# His
|
| 42 |
+
"CAT": 0.56, "CAC": 1.00,
|
| 43 |
+
# Gln
|
| 44 |
+
"CAA": 0.36, "CAG": 1.00,
|
| 45 |
+
# Asn
|
| 46 |
+
"AAT": 0.53, "AAC": 1.00,
|
| 47 |
+
# Lys
|
| 48 |
+
"AAA": 0.74, "AAG": 1.00,
|
| 49 |
+
# Asp
|
| 50 |
+
"GAT": 0.63, "GAC": 1.00,
|
| 51 |
+
# Glu
|
| 52 |
+
"GAA": 0.68, "GAG": 1.00,
|
| 53 |
+
# Cys
|
| 54 |
+
"TGT": 0.56, "TGC": 1.00,
|
| 55 |
+
# Trp
|
| 56 |
+
"TGG": 1.00,
|
| 57 |
+
# Arg
|
| 58 |
+
"CGT": 0.17, "CGC": 0.40, "CGA": 0.19, "CGG": 0.48, "AGA": 0.74, "AGG": 1.00,
|
| 59 |
+
# Gly
|
| 60 |
+
"GGT": 0.52, "GGC": 1.00, "GGA": 0.67, "GGG": 0.54,
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
_ECOLI_RSCU: Dict[str, float] = {
|
| 64 |
+
# Phe
|
| 65 |
+
"TTT": 1.00, "TTC": 0.59,
|
| 66 |
+
# Leu
|
| 67 |
+
"TTA": 0.49, "TTG": 0.74, "CTT": 0.68, "CTC": 0.39, "CTA": 0.24, "CTG": 1.00,
|
| 68 |
+
# Ile
|
| 69 |
+
"ATT": 1.00, "ATC": 0.82, "ATA": 0.19,
|
| 70 |
+
# Met
|
| 71 |
+
"ATG": 1.00,
|
| 72 |
+
# Val
|
| 73 |
+
"GTT": 1.00, "GTC": 0.60, "GTA": 0.73, "GTG": 0.72,
|
| 74 |
+
# Ser
|
| 75 |
+
"TCT": 0.92, "TCC": 0.52, "TCA": 0.46, "TCG": 0.46, "AGT": 0.72, "AGC": 1.00,
|
| 76 |
+
# Pro
|
| 77 |
+
"CCT": 0.63, "CCC": 0.27, "CCA": 0.67, "CCG": 1.00,
|
| 78 |
+
# Thr
|
| 79 |
+
"ACT": 0.95, "ACC": 1.00, "ACA": 0.47, "ACG": 0.81,
|
| 80 |
+
# Ala
|
| 81 |
+
"GCT": 0.92, "GCC": 0.70, "GCA": 0.91, "GCG": 1.00,
|
| 82 |
+
# Tyr
|
| 83 |
+
"TAT": 1.00, "TAC": 0.67,
|
| 84 |
+
# Stop
|
| 85 |
+
"TAA": 1.00, "TAG": 0.10, "TGA": 0.07,
|
| 86 |
+
# His
|
| 87 |
+
"CAT": 1.00, "CAC": 0.53,
|
| 88 |
+
# Gln
|
| 89 |
+
"CAA": 0.69, "CAG": 1.00,
|
| 90 |
+
# Asn
|
| 91 |
+
"AAT": 0.89, "AAC": 1.00,
|
| 92 |
+
# Lys
|
| 93 |
+
"AAA": 1.00, "AAG": 0.41,
|
| 94 |
+
# Asp
|
| 95 |
+
"GAT": 1.00, "GAC": 0.52,
|
| 96 |
+
# Glu
|
| 97 |
+
"GAA": 1.00, "GAG": 0.41,
|
| 98 |
+
# Cys
|
| 99 |
+
"TGT": 1.00, "TGC": 0.54,
|
| 100 |
+
# Trp
|
| 101 |
+
"TGG": 1.00,
|
| 102 |
+
# Arg
|
| 103 |
+
"CGT": 1.00, "CGC": 0.68, "CGA": 0.19, "CGG": 0.18, "AGA": 0.07, "AGG": 0.05,
|
| 104 |
+
# Gly
|
| 105 |
+
"GGT": 1.00, "GGC": 0.69, "GGA": 0.35, "GGG": 0.26,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
CODON_TABLES: Dict[str, Dict[str, float]] = {
|
| 109 |
+
"human": _HUMAN_RSCU,
|
| 110 |
+
"ecoli": _ECOLI_RSCU,
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def calculate_cai(
|
| 115 |
+
cds: str,
|
| 116 |
+
organism: str = "human",
|
| 117 |
+
custom_table: Optional[Dict[str, float]] = None,
|
| 118 |
+
) -> float:
|
| 119 |
+
"""
|
| 120 |
+
Calculate the Codon Adaptation Index for a CDS.
|
| 121 |
+
|
| 122 |
+
Parameters
|
| 123 |
+
----------
|
| 124 |
+
cds : str
|
| 125 |
+
Coding sequence (DNA, T not U). Must start with ATG and be
|
| 126 |
+
divisible by 3. Stop codon is excluded from the CAI calculation.
|
| 127 |
+
organism : str
|
| 128 |
+
Key into CODON_TABLES. Ignored if custom_table is provided.
|
| 129 |
+
custom_table : dict, optional
|
| 130 |
+
Custom {codon: relative_adaptiveness} table (values 0β1).
|
| 131 |
+
|
| 132 |
+
Returns
|
| 133 |
+
-------
|
| 134 |
+
float
|
| 135 |
+
CAI value in [0, 1]. Higher is better adapted.
|
| 136 |
+
"""
|
| 137 |
+
seq = cds.upper().replace("U", "T")
|
| 138 |
+
if len(seq) % 3 != 0:
|
| 139 |
+
raise ValueError("CDS length is not divisible by 3.")
|
| 140 |
+
|
| 141 |
+
table = custom_table if custom_table else CODON_TABLES.get(organism)
|
| 142 |
+
if table is None:
|
| 143 |
+
raise ValueError(
|
| 144 |
+
f"Unknown organism '{organism}'. "
|
| 145 |
+
f"Available: {list(CODON_TABLES.keys())}. "
|
| 146 |
+
"Provide a custom_table to use another organism."
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
codons = [seq[i:i+3] for i in range(0, len(seq), 3)]
|
| 150 |
+
# Exclude stop codons from CAI
|
| 151 |
+
stop_codons = {"TAA", "TAG", "TGA"}
|
| 152 |
+
codons = [c for c in codons if c not in stop_codons]
|
| 153 |
+
|
| 154 |
+
if not codons:
|
| 155 |
+
return 0.0
|
| 156 |
+
|
| 157 |
+
log_sum = 0.0
|
| 158 |
+
unknown = []
|
| 159 |
+
for codon in codons:
|
| 160 |
+
w = table.get(codon)
|
| 161 |
+
if w is None or w <= 0:
|
| 162 |
+
unknown.append(codon)
|
| 163 |
+
continue
|
| 164 |
+
log_sum += math.log(w)
|
| 165 |
+
|
| 166 |
+
if unknown:
|
| 167 |
+
# Non-standard codons (ambiguity codes, etc.) β skip gracefully
|
| 168 |
+
n = len(codons) - len(unknown)
|
| 169 |
+
else:
|
| 170 |
+
n = len(codons)
|
| 171 |
+
|
| 172 |
+
if n == 0:
|
| 173 |
+
return 0.0
|
| 174 |
+
|
| 175 |
+
return math.exp(log_sum / n)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def codon_usage_report(cds: str) -> Dict[str, int]:
|
| 179 |
+
"""Return a frequency count of each codon in the CDS."""
|
| 180 |
+
seq = cds.upper().replace("U", "T")
|
| 181 |
+
if len(seq) % 3 != 0:
|
| 182 |
+
raise ValueError("CDS length is not divisible by 3.")
|
| 183 |
+
freq: Dict[str, int] = {}
|
| 184 |
+
for i in range(0, len(seq), 3):
|
| 185 |
+
codon = seq[i:i+3]
|
| 186 |
+
freq[codon] = freq.get(codon, 0) + 1
|
| 187 |
+
return freq
|
core/analysis/dinucleotide.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Dinucleotide frequency analysis.
|
| 3 |
+
|
| 4 |
+
CpG dinucleotides are immunostimulatory in mammals (recognized by TLR9).
|
| 5 |
+
UpA dinucleotides are associated with mRNA instability (targeted by
|
| 6 |
+
cellular RNases). Quantifying these helps optimize mRNA therapeutics.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from typing import Dict, List, Tuple
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class DinucleotideReport:
|
| 16 |
+
"""Dinucleotide frequency analysis results."""
|
| 17 |
+
frequencies: Dict[str, int] # all dinucleotide counts
|
| 18 |
+
normalized: Dict[str, float] # frequencies / total dinucleotides
|
| 19 |
+
cpg_count: int = 0
|
| 20 |
+
upa_count: int = 0
|
| 21 |
+
cpg_positions: List[int] = field(default_factory=list)
|
| 22 |
+
upa_positions: List[int] = field(default_factory=list)
|
| 23 |
+
total_dinucleotides: int = 0
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def analyze_dinucleotides(
|
| 27 |
+
sequence: str,
|
| 28 |
+
flag_cpg: bool = True,
|
| 29 |
+
flag_upa: bool = True,
|
| 30 |
+
) -> DinucleotideReport:
|
| 31 |
+
"""
|
| 32 |
+
Analyze dinucleotide frequencies in a nucleotide sequence.
|
| 33 |
+
|
| 34 |
+
Parameters
|
| 35 |
+
----------
|
| 36 |
+
sequence : str
|
| 37 |
+
DNA or RNA sequence.
|
| 38 |
+
flag_cpg : bool
|
| 39 |
+
Track CpG positions.
|
| 40 |
+
flag_upa : bool
|
| 41 |
+
Track UpA (TpA in DNA) positions.
|
| 42 |
+
|
| 43 |
+
Returns
|
| 44 |
+
-------
|
| 45 |
+
DinucleotideReport
|
| 46 |
+
"""
|
| 47 |
+
seq = sequence.upper()
|
| 48 |
+
n = len(seq)
|
| 49 |
+
|
| 50 |
+
frequencies: Dict[str, int] = {}
|
| 51 |
+
cpg_positions: List[int] = []
|
| 52 |
+
upa_positions: List[int] = []
|
| 53 |
+
|
| 54 |
+
for i in range(n - 1):
|
| 55 |
+
di = seq[i:i + 2]
|
| 56 |
+
frequencies[di] = frequencies.get(di, 0) + 1
|
| 57 |
+
|
| 58 |
+
if flag_cpg and di == "CG":
|
| 59 |
+
cpg_positions.append(i)
|
| 60 |
+
if flag_upa and di in ("TA", "UA"):
|
| 61 |
+
upa_positions.append(i)
|
| 62 |
+
|
| 63 |
+
total = sum(frequencies.values())
|
| 64 |
+
normalized = {k: v / total if total > 0 else 0.0 for k, v in frequencies.items()}
|
| 65 |
+
|
| 66 |
+
return DinucleotideReport(
|
| 67 |
+
frequencies=frequencies,
|
| 68 |
+
normalized=normalized,
|
| 69 |
+
cpg_count=len(cpg_positions),
|
| 70 |
+
upa_count=len(upa_positions),
|
| 71 |
+
cpg_positions=cpg_positions,
|
| 72 |
+
upa_positions=upa_positions,
|
| 73 |
+
total_dinucleotides=total,
|
| 74 |
+
)
|
core/analysis/gc_content.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GC content calculation β global and sliding window."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from typing import Optional
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def gc_fraction(sequence: str) -> float:
|
| 9 |
+
"""Return global GC fraction (0.0 β 1.0) for a nucleotide sequence."""
|
| 10 |
+
seq = sequence.upper()
|
| 11 |
+
if not seq:
|
| 12 |
+
return 0.0
|
| 13 |
+
gc = sum(1 for nt in seq if nt in "GC")
|
| 14 |
+
return gc / len(seq)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def gc_percent(sequence: str) -> float:
|
| 18 |
+
"""Return global GC% (0 β 100)."""
|
| 19 |
+
return gc_fraction(sequence) * 100.0
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def gc_sliding_window(
|
| 23 |
+
sequence: str,
|
| 24 |
+
window: int = 100,
|
| 25 |
+
step: int = 1,
|
| 26 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 27 |
+
"""
|
| 28 |
+
Compute GC% in a sliding window across the sequence.
|
| 29 |
+
|
| 30 |
+
Returns
|
| 31 |
+
-------
|
| 32 |
+
positions : np.ndarray of int
|
| 33 |
+
Centre position of each window (0-based nucleotide index).
|
| 34 |
+
gc_values : np.ndarray of float
|
| 35 |
+
GC% (0β100) for each window.
|
| 36 |
+
"""
|
| 37 |
+
seq = sequence.upper()
|
| 38 |
+
n = len(seq)
|
| 39 |
+
if n == 0 or window > n:
|
| 40 |
+
return np.array([], dtype=int), np.array([], dtype=float)
|
| 41 |
+
|
| 42 |
+
# Pre-compute cumulative GC counts for O(n) sliding window
|
| 43 |
+
gc_flags = np.array([1 if nt in "GC" else 0 for nt in seq], dtype=np.int32)
|
| 44 |
+
cumsum = np.zeros(n + 1, dtype=np.int32)
|
| 45 |
+
cumsum[1:] = np.cumsum(gc_flags)
|
| 46 |
+
|
| 47 |
+
starts = np.arange(0, n - window + 1, step)
|
| 48 |
+
ends = starts + window
|
| 49 |
+
gc_counts = cumsum[ends] - cumsum[starts]
|
| 50 |
+
gc_values = gc_counts / window * 100.0
|
| 51 |
+
positions = starts + window // 2
|
| 52 |
+
|
| 53 |
+
return positions, gc_values
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def gc_by_codon_position(cds: str) -> dict[str, float]:
|
| 57 |
+
"""
|
| 58 |
+
Return GC% at each codon position (GC1, GC2, GC3).
|
| 59 |
+
cds must be in-frame and length divisible by 3.
|
| 60 |
+
"""
|
| 61 |
+
seq = cds.upper()
|
| 62 |
+
if len(seq) % 3 != 0:
|
| 63 |
+
raise ValueError("CDS length is not divisible by 3.")
|
| 64 |
+
codons = [seq[i:i+3] for i in range(0, len(seq), 3)]
|
| 65 |
+
gc1 = sum(1 for c in codons if c[0] in "GC") / len(codons) * 100
|
| 66 |
+
gc2 = sum(1 for c in codons if c[1] in "GC") / len(codons) * 100
|
| 67 |
+
gc3 = sum(1 for c in codons if c[2] in "GC") / len(codons) * 100
|
| 68 |
+
return {"GC1": gc1, "GC2": gc2, "GC3": gc3, "GC_overall": gc_percent(seq)}
|
core/analysis/homopolymers.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Homopolymer detection in nucleotide sequences."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import List
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class HomopolymerRun:
|
| 10 |
+
nucleotide: str # the repeated base ("A", "T", "G", "C")
|
| 11 |
+
start: int # 0-based start position
|
| 12 |
+
end: int # 0-based end position (exclusive)
|
| 13 |
+
length: int
|
| 14 |
+
|
| 15 |
+
def __repr__(self) -> str:
|
| 16 |
+
return f"HomopolymerRun({self.nucleotide!r} Γ{self.length} @ [{self.start}:{self.end}])"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def detect_homopolymers(
|
| 20 |
+
sequence: str,
|
| 21 |
+
min_run: int = 5,
|
| 22 |
+
bases: str = "ATGC",
|
| 23 |
+
) -> List[HomopolymerRun]:
|
| 24 |
+
"""
|
| 25 |
+
Detect homopolymer runs (consecutive identical nucleotides) in a sequence.
|
| 26 |
+
|
| 27 |
+
Parameters
|
| 28 |
+
----------
|
| 29 |
+
sequence : str
|
| 30 |
+
Nucleotide sequence (DNA or RNA).
|
| 31 |
+
min_run : int
|
| 32 |
+
Minimum run length to report (default 5).
|
| 33 |
+
bases : str
|
| 34 |
+
Which bases to check. Default "ATGC" checks all. Use "A" to detect
|
| 35 |
+
only poly-A, for example.
|
| 36 |
+
|
| 37 |
+
Returns
|
| 38 |
+
-------
|
| 39 |
+
List[HomopolymerRun]
|
| 40 |
+
Sorted by start position.
|
| 41 |
+
"""
|
| 42 |
+
seq = sequence.upper().replace("U", "T")
|
| 43 |
+
if not seq:
|
| 44 |
+
return []
|
| 45 |
+
|
| 46 |
+
runs: List[HomopolymerRun] = []
|
| 47 |
+
i = 0
|
| 48 |
+
n = len(seq)
|
| 49 |
+
|
| 50 |
+
while i < n:
|
| 51 |
+
base = seq[i]
|
| 52 |
+
if base not in bases:
|
| 53 |
+
i += 1
|
| 54 |
+
continue
|
| 55 |
+
j = i
|
| 56 |
+
while j < n and seq[j] == base:
|
| 57 |
+
j += 1
|
| 58 |
+
run_len = j - i
|
| 59 |
+
if run_len >= min_run:
|
| 60 |
+
runs.append(HomopolymerRun(
|
| 61 |
+
nucleotide=base,
|
| 62 |
+
start=i,
|
| 63 |
+
end=j,
|
| 64 |
+
length=run_len,
|
| 65 |
+
))
|
| 66 |
+
i = j
|
| 67 |
+
|
| 68 |
+
return runs
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def homopolymer_summary(runs: List[HomopolymerRun]) -> dict:
|
| 72 |
+
"""Summarise a list of HomopolymerRun objects."""
|
| 73 |
+
if not runs:
|
| 74 |
+
return {"count": 0, "max_length": 0, "longest": None}
|
| 75 |
+
longest = max(runs, key=lambda r: r.length)
|
| 76 |
+
by_base = {}
|
| 77 |
+
for r in runs:
|
| 78 |
+
by_base.setdefault(r.nucleotide, []).append(r)
|
| 79 |
+
return {
|
| 80 |
+
"count": len(runs),
|
| 81 |
+
"max_length": longest.length,
|
| 82 |
+
"longest": longest,
|
| 83 |
+
"by_base": {b: len(v) for b, v in by_base.items()},
|
| 84 |
+
}
|
core/analysis/kozak.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kozak sequence analysis.
|
| 3 |
+
|
| 4 |
+
The Kozak consensus for vertebrates is: (GCC)GCCRCCATGG
|
| 5 |
+
Where R = A or G at position -3 relative to ATG.
|
| 6 |
+
|
| 7 |
+
Scoring follows Cavener & Ray (1991) positional weight matrix approach.
|
| 8 |
+
Positions scored: -6 to +4 relative to the A of ATG (+1 = A, +2 = T, +3 = G).
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from typing import List, Optional, Tuple
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# Kozak context: positions -6 to +4 (11 nt total, ATG at [6,7,8])
|
| 17 |
+
# Positional frequency matrix derived from vertebrate Kozak sequences.
|
| 18 |
+
# Rows: A, C, G, T. Columns: positions -6 through +4.
|
| 19 |
+
# Normalised to [0, 1] (1 = dominant base at that position).
|
| 20 |
+
_PFM: List[Tuple[float, float, float, float]] = [
|
| 21 |
+
# pos: -6 -5 -4 -3 -2 -1 +1(A) +2(T) +3(G) +4
|
| 22 |
+
(0.22, 0.28, 0.28, 0.46, 0.22, 0.22, 1.00, 0.00, 0.00, 0.25), # A
|
| 23 |
+
(0.28, 0.28, 0.18, 0.12, 0.22, 0.22, 0.00, 0.00, 0.00, 0.25), # C
|
| 24 |
+
(0.22, 0.22, 0.28, 0.30, 0.22, 0.22, 0.00, 0.00, 1.00, 0.25), # G
|
| 25 |
+
(0.28, 0.22, 0.26, 0.12, 0.34, 0.34, 0.00, 1.00, 0.00, 0.25), # T
|
| 26 |
+
]
|
| 27 |
+
_BASES = "ACGT"
|
| 28 |
+
_CONTEXT_LEN = 10 # positions -6 through +4 (10 positions around ATG start)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class KozakResult:
|
| 33 |
+
"""Result of Kozak consensus analysis for one ATG."""
|
| 34 |
+
atg_position: int # 0-based position of A in ATG within the full sequence
|
| 35 |
+
context: str # extracted Kozak context window
|
| 36 |
+
score: float # normalised score 0β1 (1 = perfect consensus)
|
| 37 |
+
has_optimal_r3: bool # A or G at position -3
|
| 38 |
+
matches_consensus: bool # True if score > 0.7 threshold
|
| 39 |
+
strength: str # "strong", "adequate", "weak"
|
| 40 |
+
|
| 41 |
+
def __repr__(self) -> str:
|
| 42 |
+
return (
|
| 43 |
+
f"KozakResult(pos={self.atg_position}, "
|
| 44 |
+
f"context={self.context!r}, score={self.score:.2f}, "
|
| 45 |
+
f"strength={self.strength!r})"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _score_context(context: str) -> float:
|
| 50 |
+
"""Score a 10-nt Kozak context window against the PFM."""
|
| 51 |
+
if len(context) != 10:
|
| 52 |
+
return 0.0
|
| 53 |
+
total = 0.0
|
| 54 |
+
for i, nt in enumerate(context.upper()):
|
| 55 |
+
if nt not in _BASES:
|
| 56 |
+
continue
|
| 57 |
+
row = _BASES.index(nt)
|
| 58 |
+
total += _PFM[row][i]
|
| 59 |
+
# Max possible score: 10 (1.0 per position)
|
| 60 |
+
return total / 10.0
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _strength(score: float) -> str:
|
| 64 |
+
# Thresholds calibrated against achievable scores with the PFM above.
|
| 65 |
+
# Max achievable for ideal Kozak (GCCACCATGG) β 0.48.
|
| 66 |
+
if score >= 0.43:
|
| 67 |
+
return "strong"
|
| 68 |
+
if score >= 0.33:
|
| 69 |
+
return "adequate"
|
| 70 |
+
return "weak"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def check_kozak(sequence: str, atg_position: Optional[int] = None) -> KozakResult:
|
| 74 |
+
"""
|
| 75 |
+
Analyse the Kozak context around the first (or specified) ATG in the sequence.
|
| 76 |
+
|
| 77 |
+
Parameters
|
| 78 |
+
----------
|
| 79 |
+
sequence : str
|
| 80 |
+
Nucleotide sequence (DNA).
|
| 81 |
+
atg_position : int, optional
|
| 82 |
+
0-based position of the ATG to analyse. If None, uses the first ATG found.
|
| 83 |
+
|
| 84 |
+
Returns
|
| 85 |
+
-------
|
| 86 |
+
KozakResult
|
| 87 |
+
"""
|
| 88 |
+
seq = sequence.upper().replace("U", "T")
|
| 89 |
+
|
| 90 |
+
if atg_position is None:
|
| 91 |
+
pos = seq.find("ATG")
|
| 92 |
+
if pos == -1:
|
| 93 |
+
raise ValueError("No ATG start codon found in sequence.")
|
| 94 |
+
else:
|
| 95 |
+
pos = atg_position
|
| 96 |
+
if seq[pos:pos+3] != "ATG":
|
| 97 |
+
raise ValueError(f"No ATG at position {pos}.")
|
| 98 |
+
|
| 99 |
+
# Extract context: 6 nt before ATG + ATG + 1 nt after = 10 nt total
|
| 100 |
+
ctx_start = pos - 6
|
| 101 |
+
ctx_end = pos + 4 # positions -6 to +4 (ATG at indices 6,7,8 of the 10-nt window)
|
| 102 |
+
# Pad with N if near sequence edges
|
| 103 |
+
left_pad = max(0, -ctx_start) * "N"
|
| 104 |
+
right_pad = max(0, ctx_end - len(seq)) * "N"
|
| 105 |
+
actual_start = max(0, ctx_start)
|
| 106 |
+
actual_end = min(len(seq), ctx_end)
|
| 107 |
+
context = left_pad + seq[actual_start:actual_end] + right_pad
|
| 108 |
+
|
| 109 |
+
score = _score_context(context)
|
| 110 |
+
|
| 111 |
+
# -3 position relative to ATG = index 3 in the 10-nt context
|
| 112 |
+
r3_base = context[3] if len(context) > 3 else "N"
|
| 113 |
+
has_r3 = r3_base in "AG"
|
| 114 |
+
|
| 115 |
+
return KozakResult(
|
| 116 |
+
atg_position=pos,
|
| 117 |
+
context=context,
|
| 118 |
+
score=score,
|
| 119 |
+
has_optimal_r3=has_r3,
|
| 120 |
+
matches_consensus=score >= 0.55,
|
| 121 |
+
strength=_strength(score),
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def find_all_kozak_contexts(sequence: str, min_score: float = 0.0) -> List[KozakResult]:
|
| 126 |
+
"""Find and score Kozak contexts for every ATG in the sequence."""
|
| 127 |
+
seq = sequence.upper().replace("U", "T")
|
| 128 |
+
results = []
|
| 129 |
+
start = 0
|
| 130 |
+
while True:
|
| 131 |
+
pos = seq.find("ATG", start)
|
| 132 |
+
if pos == -1:
|
| 133 |
+
break
|
| 134 |
+
result = check_kozak(seq, pos)
|
| 135 |
+
if result.score >= min_score:
|
| 136 |
+
results.append(result)
|
| 137 |
+
start = pos + 1
|
| 138 |
+
return results
|
core/analysis/restriction_sites.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Restriction enzyme site scanning.
|
| 3 |
+
|
| 4 |
+
Uses BioPython's Restriction module for recognition site data.
|
| 5 |
+
Falls back to a small curated built-in table for the most common
|
| 6 |
+
cloning enzymes if BioPython is unavailable.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from typing import Dict, List, Optional
|
| 12 |
+
|
| 13 |
+
# Built-in recognition patterns for common enzymes (IUPAC notation)
|
| 14 |
+
# Used as fallback and for quick lookups without full Bio.Restriction import.
|
| 15 |
+
COMMON_ENZYMES: Dict[str, str] = {
|
| 16 |
+
# Type IIS / Golden Gate
|
| 17 |
+
"BsaI": "GGTCTC",
|
| 18 |
+
"BbsI": "GAAGAC",
|
| 19 |
+
"Esp3I": "CGTCTC",
|
| 20 |
+
"SapI": "GCTCTTC",
|
| 21 |
+
"BsmBI": "CGTCTC",
|
| 22 |
+
# Classic cloning
|
| 23 |
+
"EcoRI": "GAATTC",
|
| 24 |
+
"HindIII":"AAGCTT",
|
| 25 |
+
"BamHI": "GGATCC",
|
| 26 |
+
"NcoI": "CCATGG",
|
| 27 |
+
"NheI": "GCTAGC",
|
| 28 |
+
"XhoI": "CTCGAG",
|
| 29 |
+
"XbaI": "TCTAGA",
|
| 30 |
+
"SpeI": "ACTAGT",
|
| 31 |
+
"NotI": "GCGGCCGC",
|
| 32 |
+
"SalI": "GTCGAC",
|
| 33 |
+
"PstI": "CTGCAG",
|
| 34 |
+
"KpnI": "GGTACC",
|
| 35 |
+
"SmaI": "CCCGGG",
|
| 36 |
+
"SacI": "GAGCTC",
|
| 37 |
+
"ClaI": "ATCGAT",
|
| 38 |
+
# Blunt cutters
|
| 39 |
+
"EcoRV": "GATATC",
|
| 40 |
+
"HpaI": "GTTAAC",
|
| 41 |
+
"StuI": "AGGCCT",
|
| 42 |
+
"ScaI": "AGTACT",
|
| 43 |
+
# Methylation-sensitive
|
| 44 |
+
"DpnI": "GATC",
|
| 45 |
+
"MboI": "GATC",
|
| 46 |
+
"Sau3AI": "GATC",
|
| 47 |
+
# Rare cutters (8+ bp)
|
| 48 |
+
"SfiI": "GGCCNNNNNGGCC",
|
| 49 |
+
"PacI": "TTAATTAA",
|
| 50 |
+
"AscI": "GGCGCGCC",
|
| 51 |
+
"FseI": "GGCCGGCC",
|
| 52 |
+
"SwaI": "ATTTAAAT",
|
| 53 |
+
"PmeI": "GTTTAAAC",
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
# IUPAC ambiguity β regex character class
|
| 57 |
+
_IUPAC_TO_REGEX: Dict[str, str] = {
|
| 58 |
+
"A": "A", "T": "T", "G": "G", "C": "C",
|
| 59 |
+
"R": "[AG]", "Y": "[CT]", "S": "[GC]", "W": "[AT]",
|
| 60 |
+
"K": "[GT]", "M": "[AC]", "B": "[CGT]", "D": "[AGT]",
|
| 61 |
+
"H": "[ACT]", "V": "[ACG]", "N": "[ACGT]",
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _iupac_to_regex(pattern: str) -> str:
|
| 66 |
+
import re
|
| 67 |
+
return "".join(_IUPAC_TO_REGEX.get(c, c) for c in pattern.upper())
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@dataclass
|
| 71 |
+
class RestrictionSiteHit:
|
| 72 |
+
enzyme: str
|
| 73 |
+
recognition_sequence: str
|
| 74 |
+
position: int # 0-based start of recognition sequence on forward strand
|
| 75 |
+
strand: str # "+" forward, "-" reverse complement
|
| 76 |
+
|
| 77 |
+
def __repr__(self) -> str:
|
| 78 |
+
return (
|
| 79 |
+
f"RestrictionSiteHit({self.enzyme!r} @ pos {self.position} "
|
| 80 |
+
f"strand={self.strand!r})"
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _reverse_complement(seq: str) -> str:
|
| 85 |
+
comp = str.maketrans("ATGCRYSWKMBDHVN", "TACGYRSWMKVHDBN")
|
| 86 |
+
return seq.upper().translate(comp)[::-1]
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def scan_restriction_sites(
|
| 90 |
+
sequence: str,
|
| 91 |
+
enzymes: Optional[List[str]] = None,
|
| 92 |
+
) -> Dict[str, List[RestrictionSiteHit]]:
|
| 93 |
+
"""
|
| 94 |
+
Scan a DNA sequence for restriction enzyme recognition sites.
|
| 95 |
+
|
| 96 |
+
Parameters
|
| 97 |
+
----------
|
| 98 |
+
sequence : str
|
| 99 |
+
DNA sequence to scan.
|
| 100 |
+
enzymes : list of str, optional
|
| 101 |
+
Enzyme names to check. Defaults to COMMON_ENZYMES.
|
| 102 |
+
|
| 103 |
+
Returns
|
| 104 |
+
-------
|
| 105 |
+
dict
|
| 106 |
+
{enzyme_name: [RestrictionSiteHit, ...]}
|
| 107 |
+
Only enzymes with at least one hit are included.
|
| 108 |
+
"""
|
| 109 |
+
import re
|
| 110 |
+
|
| 111 |
+
seq = sequence.upper().replace("U", "T")
|
| 112 |
+
rc_seq = _reverse_complement(seq)
|
| 113 |
+
n = len(seq)
|
| 114 |
+
|
| 115 |
+
enzyme_list = enzymes if enzymes else list(COMMON_ENZYMES.keys())
|
| 116 |
+
results: Dict[str, List[RestrictionSiteHit]] = {}
|
| 117 |
+
|
| 118 |
+
for enzyme in enzyme_list:
|
| 119 |
+
recognition = COMMON_ENZYMES.get(enzyme)
|
| 120 |
+
if not recognition:
|
| 121 |
+
# Try BioPython if available
|
| 122 |
+
try:
|
| 123 |
+
from Bio.Restriction import AllEnzymes
|
| 124 |
+
enz_obj = AllEnzymes.get(enzyme)
|
| 125 |
+
if enz_obj:
|
| 126 |
+
recognition = str(enz_obj.site)
|
| 127 |
+
else:
|
| 128 |
+
continue
|
| 129 |
+
except ImportError:
|
| 130 |
+
continue
|
| 131 |
+
|
| 132 |
+
regex = _iupac_to_regex(recognition)
|
| 133 |
+
hits: List[RestrictionSiteHit] = []
|
| 134 |
+
|
| 135 |
+
# Forward strand
|
| 136 |
+
for m in re.finditer(f"(?={regex})", seq):
|
| 137 |
+
hits.append(RestrictionSiteHit(
|
| 138 |
+
enzyme=enzyme,
|
| 139 |
+
recognition_sequence=recognition,
|
| 140 |
+
position=m.start(),
|
| 141 |
+
strand="+",
|
| 142 |
+
))
|
| 143 |
+
|
| 144 |
+
# Reverse complement strand (report position on forward)
|
| 145 |
+
rev_rec = _iupac_to_regex(_reverse_complement(recognition))
|
| 146 |
+
for m in re.finditer(f"(?={rev_rec})", seq):
|
| 147 |
+
hits.append(RestrictionSiteHit(
|
| 148 |
+
enzyme=enzyme,
|
| 149 |
+
recognition_sequence=recognition,
|
| 150 |
+
position=m.start(),
|
| 151 |
+
strand="-",
|
| 152 |
+
))
|
| 153 |
+
|
| 154 |
+
if hits:
|
| 155 |
+
results[enzyme] = sorted(hits, key=lambda h: h.position)
|
| 156 |
+
|
| 157 |
+
return results
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def sites_present(
|
| 161 |
+
sequence: str,
|
| 162 |
+
enzymes: Optional[List[str]] = None,
|
| 163 |
+
) -> List[str]:
|
| 164 |
+
"""Return list of enzyme names that have at least one hit in the sequence."""
|
| 165 |
+
return list(scan_restriction_sites(sequence, enzymes).keys())
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def sites_absent(
|
| 169 |
+
sequence: str,
|
| 170 |
+
required_absent: List[str],
|
| 171 |
+
) -> List[str]:
|
| 172 |
+
"""Return list of enzymes from required_absent that ARE present (i.e. violations)."""
|
| 173 |
+
present = set(sites_present(sequence, required_absent))
|
| 174 |
+
return [e for e in required_absent if e in present]
|
core/analysis/structure.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
RNA secondary structure prediction via ViennaRNA.
|
| 3 |
+
|
| 4 |
+
ViennaRNA (RNA package) must be installed:
|
| 5 |
+
conda install -c bioconda viennarna
|
| 6 |
+
or: pip install ViennaRNA (if wheel available for the platform)
|
| 7 |
+
|
| 8 |
+
Falls back to a stub when ViennaRNA is not available so the rest of the
|
| 9 |
+
app can run without it.
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
from typing import Optional
|
| 15 |
+
|
| 16 |
+
_VIENNARNA_AVAILABLE = False
|
| 17 |
+
try:
|
| 18 |
+
import RNA # type: ignore[import-untyped]
|
| 19 |
+
_VIENNARNA_AVAILABLE = True
|
| 20 |
+
except ImportError:
|
| 21 |
+
pass
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class StructureResult:
|
| 26 |
+
sequence: str
|
| 27 |
+
structure: str # dot-bracket notation
|
| 28 |
+
mfe: float # minimum free energy (kcal/mol)
|
| 29 |
+
ensemble_free_energy: Optional[float] = None
|
| 30 |
+
centroid_structure: Optional[str] = None
|
| 31 |
+
centroid_distance: Optional[float] = None
|
| 32 |
+
|
| 33 |
+
@property
|
| 34 |
+
def is_stub(self) -> bool:
|
| 35 |
+
return self.structure == "" and self.mfe == 0.0
|
| 36 |
+
|
| 37 |
+
def __repr__(self) -> str:
|
| 38 |
+
return (
|
| 39 |
+
f"StructureResult(mfe={self.mfe:.2f} kcal/mol, "
|
| 40 |
+
f"len={len(self.sequence)})"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def predict_structure(sequence: str) -> StructureResult:
|
| 45 |
+
"""
|
| 46 |
+
Predict the MFE secondary structure of an RNA/DNA sequence.
|
| 47 |
+
|
| 48 |
+
The sequence is automatically converted from DNA to RNA (TβU) before
|
| 49 |
+
passing to ViennaRNA, which expects RNA input.
|
| 50 |
+
|
| 51 |
+
Returns a StructureResult. If ViennaRNA is not installed, returns a
|
| 52 |
+
stub result with empty structure and mfe=0.0.
|
| 53 |
+
"""
|
| 54 |
+
rna_seq = sequence.upper().replace("T", "U")
|
| 55 |
+
|
| 56 |
+
if not _VIENNARNA_AVAILABLE:
|
| 57 |
+
return StructureResult(
|
| 58 |
+
sequence=rna_seq,
|
| 59 |
+
structure="",
|
| 60 |
+
mfe=0.0,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# MFE structure
|
| 64 |
+
structure, mfe = RNA.fold(rna_seq) # type: ignore[attr-defined]
|
| 65 |
+
|
| 66 |
+
# Ensemble / centroid (for longer seqs this is informative)
|
| 67 |
+
md = RNA.md() # type: ignore[attr-defined]
|
| 68 |
+
fc = RNA.fold_compound(rna_seq, md) # type: ignore[attr-defined]
|
| 69 |
+
_, ensemble_free_energy = fc.pf()
|
| 70 |
+
centroid_structure, centroid_distance = fc.centroid()
|
| 71 |
+
|
| 72 |
+
return StructureResult(
|
| 73 |
+
sequence=rna_seq,
|
| 74 |
+
structure=structure,
|
| 75 |
+
mfe=mfe,
|
| 76 |
+
ensemble_free_energy=ensemble_free_energy,
|
| 77 |
+
centroid_structure=centroid_structure,
|
| 78 |
+
centroid_distance=centroid_distance,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def is_viennarna_available() -> bool:
|
| 83 |
+
return _VIENNARNA_AVAILABLE
|
core/analysis/uridine.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Uridine content analysis for mRNA sequences.
|
| 3 |
+
|
| 4 |
+
High uridine content can trigger innate immune responses. Modified
|
| 5 |
+
nucleotides (N1-methylpseudouridine) mitigate this, but it's still
|
| 6 |
+
useful to quantify uridine distribution.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from typing import List, Tuple
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class UridineReport:
|
| 16 |
+
"""Uridine content analysis results."""
|
| 17 |
+
total_u: int
|
| 18 |
+
total_length: int
|
| 19 |
+
u_fraction: float # 0.0 β 1.0
|
| 20 |
+
u_percent: float # 0 β 100
|
| 21 |
+
ua_ratio: float # U/A ratio
|
| 22 |
+
high_u_stretches: List[Tuple[int, int, int]] # (start, end, length) of high-U regions
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def analyze_uridine(
|
| 26 |
+
sequence: str,
|
| 27 |
+
window: int = 50,
|
| 28 |
+
threshold: float = 0.40,
|
| 29 |
+
) -> UridineReport:
|
| 30 |
+
"""
|
| 31 |
+
Analyze uridine content in an RNA/DNA sequence.
|
| 32 |
+
|
| 33 |
+
Parameters
|
| 34 |
+
----------
|
| 35 |
+
sequence : str
|
| 36 |
+
Nucleotide sequence (DNA or RNA).
|
| 37 |
+
window : int
|
| 38 |
+
Sliding window size for high-U stretch detection.
|
| 39 |
+
threshold : float
|
| 40 |
+
U fraction threshold for flagging high-U windows.
|
| 41 |
+
|
| 42 |
+
Returns
|
| 43 |
+
-------
|
| 44 |
+
UridineReport
|
| 45 |
+
"""
|
| 46 |
+
seq = sequence.upper().replace("T", "U")
|
| 47 |
+
n = len(seq)
|
| 48 |
+
|
| 49 |
+
total_u = seq.count("U")
|
| 50 |
+
total_a = seq.count("A")
|
| 51 |
+
|
| 52 |
+
u_fraction = total_u / n if n > 0 else 0.0
|
| 53 |
+
ua_ratio = total_u / total_a if total_a > 0 else float("inf")
|
| 54 |
+
|
| 55 |
+
# Find high-U stretches using sliding window
|
| 56 |
+
stretches: List[Tuple[int, int, int]] = []
|
| 57 |
+
if n >= window:
|
| 58 |
+
in_stretch = False
|
| 59 |
+
stretch_start = 0
|
| 60 |
+
for i in range(n - window + 1):
|
| 61 |
+
win = seq[i:i + window]
|
| 62 |
+
u_frac = win.count("U") / window
|
| 63 |
+
if u_frac >= threshold:
|
| 64 |
+
if not in_stretch:
|
| 65 |
+
stretch_start = i
|
| 66 |
+
in_stretch = True
|
| 67 |
+
else:
|
| 68 |
+
if in_stretch:
|
| 69 |
+
stretches.append((stretch_start, i + window - 1, i + window - 1 - stretch_start))
|
| 70 |
+
in_stretch = False
|
| 71 |
+
if in_stretch:
|
| 72 |
+
stretches.append((stretch_start, n, n - stretch_start))
|
| 73 |
+
|
| 74 |
+
return UridineReport(
|
| 75 |
+
total_u=total_u,
|
| 76 |
+
total_length=n,
|
| 77 |
+
u_fraction=u_fraction,
|
| 78 |
+
u_percent=u_fraction * 100,
|
| 79 |
+
ua_ratio=ua_ratio,
|
| 80 |
+
high_u_stretches=stretches,
|
| 81 |
+
)
|
core/data/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Seed data for the application."""
|
core/data/seed_backbones.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Seed plasmid backbones for the assembly workflow.
|
| 3 |
+
|
| 4 |
+
Provides demo backbone data for common expression vectors.
|
| 5 |
+
Sequences are truncated/representative for demo purposes.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from typing import List
|
| 10 |
+
|
| 11 |
+
from core.models.plasmid import PlasmidBackbone, PlasmidFeature
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def get_seed_backbones() -> List[PlasmidBackbone]:
|
| 15 |
+
"""Return demo plasmid backbones."""
|
| 16 |
+
backbones = []
|
| 17 |
+
|
| 18 |
+
# pUC19 β classic high-copy cloning vector
|
| 19 |
+
backbones.append(PlasmidBackbone(
|
| 20 |
+
name="pUC19",
|
| 21 |
+
description="High-copy number E. coli cloning vector (2,686 bp). Contains lacZ-alpha MCS, ampicillin resistance, and pMB1 origin.",
|
| 22 |
+
sequence="TCGCGCGTTTCGGTGATGACGGTGAAAACCTCTGACACATGCAGCTCCCGGAGACGGTCACAGCTTGTCTGTAAGCGGATGCCGGGAGCAGACAAGCCCGTCAGGGCGCGTCAGCGGGTGTTGGCGGGTGTCGGGGCTGGCTTAACTATGCGGCATCAGAGCAGATTGTACTGAGAGTGCACCATATGCGGTGTGAAATACCGCACAGATGCGTAAGGAGAAAATACCGCATCAGGCGCCATTCGCCATTCAGGCTGCGCAACTGTTGGGAAGGGCGATCGGTGCGGGCCTCTTCGCTATTACGCCAGCTGGCGAAAGGGGGATGTGCTGCAAGGCGATTAAGTTGGGTAACGCCAGGGTTTTCCCAGTCACGACGTTGTAAAACGACGGCCAGTGAATTCGAGCTCGGTACCCGGGGATCCTCTAGAGTCGACCTGCAGGCATGCAAGCTTGGCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCACAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGCCTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTTCCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGCGGGGAGAGGCGGTTTGCGTATTGGGCGCTCTTCCGCTTCCTCGCTCACTGACTCGCTGCGCTCGGTCGTTCGGCTGCGGCGAGCGGTATCAGCTCACTCAAAGGCGGTAATACGGTTATCCACAGAATCAGGGGATAACGCAGGAAAGAACATGTGAGCAAAAGGCCAGCAAAAGGCCAGGAACCGTAAAAAGGCCGCGTTGCTGGCGTTTTTCCATAGGCTCCGCCCCCCTGACGAGCATCACAAAAATCGACGCTCAAGTCAGAGGTGGCGAAACCCGACAGGACTATAAAGATACCAGGCGTTTCCCCCTGGAAGCTCCCTCGTGCGCTCTCCTGTTCCGACCCTGCCGCTTACCGGATACCTGTCCGCCTTTCTCCCTTCGGGAAGCGTGGCGCTTTCTCATAGCTCACGCTGTAGGTATCTCAGTTCGGTGTAGGTCGTTCGCTCCAAGCTGGGCTGTGTGCACGAACCCCCCGTTCAGCCCGACCGCTGCGCCTTATCCGGTAACTATCGTCTTGAGTCCAACCCGGTAAGACACGACTTATCGCCACTGGCAGCAGCCACTGGTAACAGGATTAGCAGAGCGAGGTATGTAGGCGGTGCTACAGAGTTCTTGAAGTGGTGGCCTAACTACGGCTACACTAGAAGAACAGTATTTGGTATCTGCGCTCTGCTGAAGCCAGTTACCTTCGGAAAAAGAGTTGGTAGCTCTTGATCCGGCAAACAAACCACCGCTGGTAGCGGTGGTTTTTTTGTTTGCAAGCAGCAGATTACGCGCAGAAAAAAAGGATCTCAAGAAGATCCTTTGATCTTTTCTACGGGGTCTGACGCTCAGTGGAACGAAAACTCACGTTAAGGGATTTTGGTCATGAGATTATCAAAAAGGATCTTCACCTAGATCCTTTTAAATTAAAAATGAAGTTTTAAATCAATCTAAAGTATATATGAGTAAACTTGGTCTGACAGTTACCAATGCTTAATCAGTGAGGCACCTATCTCAGCGATCTGTCTATTTCGTTCATCCATAGTTGCCTGACTCCCCGTCGTGTAGATAACTACGATACGGGAGGGCTTACCATCTGGCCCCAGTGCTGCAATGATACCGCGAGACCCACGCTCACCGGCTCCAGATTTATCAGCAATAAACCAGCCAGCCGGAAGGGCCGAGCGCAGAAGTGGTCCTGCAACTTTATCCGCCTCCATCCAGTCTATTAATTGTTGCCGGGAAGCTAGAGTAAGTAGTTCGCCAGTTAATAGTTTGCGCAACGTTGTTGCCATTGCTACAGGCATCGTGGTGTCACGCTCGTCGTTTGGTATGGCTTCATTCAGCTCCGGTTCCCAACGATCAAGGCGAGTTACATGATCCCCCATGTTGTGCAAAAAAGCGGTTAGCTCCTTCGGTCCTCCGATCGTTGTCAGAAGTAAGTTGGCCGCAGTGTTATCACTCATGGTTATGGCAGCACTGCATAATTCTCTTACTGTCATGCCATCCGTAAGATGCTTTTCTGTGACTGGTGAGTACTCAACCAAGTCATTCTGAGAATAGTGTATGCGGCGACCGAGTTGCTCTTGCCCGGCGTCAATACGGGATAATACCGCGCCACATAGCAGAACTTTAAAAGTGCTCATCATTGGAAAACGTTCTTCGGGGCGAAAACTCTCAAGGATCTTACCGCTGTTGAGATCCAGTTCGATGTAACCCACTCGTGCACCCAACTGATCTTCAGCATCTTTTACTTTCACCAGCGTTTCTGGGTGAGCAAAAACAGGAAGGCAAAATGCCGCAAAAAAGGGAATAAGGGCGACACGGAAATGTTGAATACTCATACTCTTCCTTTTTCAATATTATTGAAGCATTTATCAGGGTTATTGTCTCATGAGCGGATACATATTTGAATGTATTTAGAAAAATAAACAAATAGGGGTTCCGCGCACATTTCCCCGAAAAGTGCCACCTGACGTC",
|
| 23 |
+
features=[
|
| 24 |
+
PlasmidFeature(label="AmpR", feature_type="resistance", start=1629, end=2489, strand="-", color="#EF4444"),
|
| 25 |
+
PlasmidFeature(label="pMB1 ori", feature_type="ori", start=2489, end=2686, color="#3B82F6"),
|
| 26 |
+
PlasmidFeature(label="lacZ-alpha", feature_type="other", start=217, end=580, color="#10B981"),
|
| 27 |
+
PlasmidFeature(label="MCS", feature_type="cloning_site", start=396, end=452, color="#F59E0B"),
|
| 28 |
+
],
|
| 29 |
+
cloning_sites=["EcoRI", "SacI", "KpnI", "BamHI", "XbaI", "SalI", "PstI", "SphI", "HindIII"],
|
| 30 |
+
source="library",
|
| 31 |
+
))
|
| 32 |
+
|
| 33 |
+
# pcDNA3.1 β mammalian expression vector
|
| 34 |
+
backbones.append(PlasmidBackbone(
|
| 35 |
+
name="pcDNA3.1(+)",
|
| 36 |
+
description="Mammalian expression vector (5,428 bp). CMV promoter, BGH polyA, neomycin/G418 resistance, SV40 ori.",
|
| 37 |
+
sequence="GACGGATCGGGAGATCTCCCGATCCCCTATGGTGCACTCTCAGTACAATCTGCTCTGATGCCGCATAGTTAAGCCAGTATCTGCTCCCTGCTTGTGTGTTGGAGGTCGCTGAGTAGTGCGCGAGCAAAATTTAAGCTACAACAAGGCAAGGCTTGACCGACAATTGCATGAAGAATCTGCTTAGGGTTAGGCGTTTTGCGCTGCTTCGCGATGTACGGGCCAGATATACGCGTTGACATTGATTATTGACTAGTTATTAATAGTAATCAATTACGGGGTCATTAGTTCATAGCCCATATATGGAGTTCCGCGTTACATAACTTACGGTAAATGGCCCGCCTGGCTGACCGCCCAACGACCCCCGCCCATTGACGTCAATAATGACGTATGTTCCCATAGTAACGCCAATAGGGACTTTCCATTGACGTCAATGGGTGGAGTATTTACGGTAAACTGCCCACTTGGCAGTACATCAAGTGTATCATATGCCAAGTACGCCCCCTATTGACGTCAATGACGGTAAATGGCCCGCCTGGCATTATGCCCAGTACATGACCTTATGGGACTTTCCTACTTGGCAGTACATCTACGTATTAGTCATCGCTATTACCATGGTGATGCGGTTTTGGCAGTACATCAATGGGCGTGGATAGCGGTTTGACTCACGGGGATTTCCAAGTCTCCACCCCATTGACGTCAATGGGAGTTTGTTTTGGCACCAAAATCAACGGGACTTTCCAAAATGTCGTAACAACTCCGCCCCATTGACGCAAATGGGCGGTAGGCGTGTACGGTGGGAGGTCTATATAAGCAGAGCTGGTTTAGTGAACCGTCAGATC" + "A" * 200,
|
| 38 |
+
features=[
|
| 39 |
+
PlasmidFeature(label="CMV promoter", feature_type="promoter", start=1, end=590, color="#8B5CF6"),
|
| 40 |
+
PlasmidFeature(label="MCS", feature_type="cloning_site", start=895, end=1010, color="#F59E0B"),
|
| 41 |
+
PlasmidFeature(label="BGH polyA", feature_type="terminator", start=1010, end=1234, color="#EF4444"),
|
| 42 |
+
PlasmidFeature(label="NeoR/KanR", feature_type="resistance", start=1850, end=2644, strand="+", color="#10B981"),
|
| 43 |
+
PlasmidFeature(label="SV40 ori", feature_type="ori", start=1234, end=1450, color="#3B82F6"),
|
| 44 |
+
],
|
| 45 |
+
cloning_sites=["NheI", "BamHI", "EcoRI", "EcoRV", "BstXI", "NotI", "XhoI", "XbaI", "ApaI", "HindIII", "KpnI"],
|
| 46 |
+
source="library",
|
| 47 |
+
))
|
| 48 |
+
|
| 49 |
+
# pET-28a β bacterial expression with His-tag
|
| 50 |
+
backbones.append(PlasmidBackbone(
|
| 51 |
+
name="pET-28a(+)",
|
| 52 |
+
description="E. coli T7 expression vector (5,369 bp). N-terminal His-tag, T7 promoter, kanamycin resistance.",
|
| 53 |
+
sequence="ATCCGGATATAGTTCCTCCTTTCAGCAAAAAACCCCTCAAGACCCGTTTAGAGGCCCCAAGGGGTTATGCTAGTTATTGCTCAGCGGTGGCAGCAGCCAACTCAGCTTCCTTTCGGGCTTTGTTAGCAGCCGGATCTCAGTGGTGGTGGTGGTGGTGCTCGAGTGCGGCCGCAAGCTTGTCGACGGAGCTCGAATTCGGATCCGATATCGAATTCCTGCAGCCCGGGGGATCCACTAGTTCTAGAGCGGCCGCCACCGCGGTGGAGCTCCAGCTTTTGTTCCCTTTAGTGAGGGTTAATTGCGCGCTTGGCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCACAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGCCTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTTCCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGCGGGGAGAGGCGGTTTGCGTATTGGGCGCTCTTCCGCTTCCTCGCTCACTGACTCGCTGCGCTCGGTCGTTCGGCTGCGGCGAGCGGTATCAGCTCACTCAAAGGCGGTAATACGGTTATCCACAGAATCAGGGGATAACGCAGGAAAGAACATGTGAGCAAAAGGCCAGCAAAAGGCCAGGAACCGTAAAAAGGCCGCGTTGCTGGCG" + "T" * 100,
|
| 54 |
+
features=[
|
| 55 |
+
PlasmidFeature(label="T7 promoter", feature_type="promoter", start=1, end=50, color="#8B5CF6"),
|
| 56 |
+
PlasmidFeature(label="His-tag", feature_type="tag", start=50, end=80, color="#F59E0B"),
|
| 57 |
+
PlasmidFeature(label="MCS", feature_type="cloning_site", start=80, end=200, color="#F59E0B"),
|
| 58 |
+
PlasmidFeature(label="KanR", feature_type="resistance", start=500, end=1300, color="#10B981"),
|
| 59 |
+
PlasmidFeature(label="pBR322 ori", feature_type="ori", start=1300, end=1800, color="#3B82F6"),
|
| 60 |
+
],
|
| 61 |
+
cloning_sites=["NcoI", "NdeI", "BamHI", "EcoRI", "SalI", "HindIII", "NotI", "XhoI", "NheI"],
|
| 62 |
+
source="library",
|
| 63 |
+
))
|
| 64 |
+
|
| 65 |
+
# pGEM-T β TA cloning vector
|
| 66 |
+
backbones.append(PlasmidBackbone(
|
| 67 |
+
name="pGEM-T Easy",
|
| 68 |
+
description="TA cloning vector (3,015 bp). T7 and SP6 promoters flanking MCS within lacZ, ampicillin resistance.",
|
| 69 |
+
sequence="GGGCGAATTGGGCCCGACGTCGCATGCTCCCGGCCGCCATGGCGGCCGCGGGAATTCGATTTAAATCTAGAGTCGACCTGCAGGCATGCAAGCTTGAGTATTCTATAGTGTCACCTAAATAGCTTGGCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCACAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGCCTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTTCCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGCGGGGAGAGGCGGTTTGCGTATTGGGCGCTCTTCCGCTTCCTCGCTCACTGACTCGCTGCGCTCGGTCGTTCGGCTGCGGCGAGCGGTATCAGCTCACTCAAAGGCGGTAATACGGTTATCCACAGAATCAGGGGATAACGCAGGAAAGAACAT" + "A" * 100,
|
| 70 |
+
features=[
|
| 71 |
+
PlasmidFeature(label="T7 promoter", feature_type="promoter", start=1, end=30, color="#8B5CF6"),
|
| 72 |
+
PlasmidFeature(label="SP6 promoter", feature_type="promoter", start=180, end=210, strand="-", color="#8B5CF6"),
|
| 73 |
+
PlasmidFeature(label="MCS", feature_type="cloning_site", start=30, end=180, color="#F59E0B"),
|
| 74 |
+
PlasmidFeature(label="AmpR", feature_type="resistance", start=350, end=1200, color="#EF4444"),
|
| 75 |
+
],
|
| 76 |
+
cloning_sites=["EcoRI", "SacI", "BstXI", "NotI", "SalI", "NcoI", "BamHI", "SpeI", "ApaI"],
|
| 77 |
+
source="library",
|
| 78 |
+
))
|
| 79 |
+
|
| 80 |
+
return backbones
|
core/data/seed_parts.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Seed parts library with common, well-characterized sequence components.
|
| 3 |
+
|
| 4 |
+
These are loaded on app startup to provide a starting library for users.
|
| 5 |
+
"""
|
| 6 |
+
from typing import List
|
| 7 |
+
|
| 8 |
+
from core.models.parts import UTR5Part, KozakPart, CDSPart, UTR3Part, PolyAPart, SequencePart
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def get_seed_parts() -> List[SequencePart]:
|
| 12 |
+
"""Return a list of seed parts to populate the library."""
|
| 13 |
+
|
| 14 |
+
parts = []
|
| 15 |
+
|
| 16 |
+
# ββ 5' UTRs ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 17 |
+
|
| 18 |
+
# Strong constitutive 5'UTR from Ξ²-globin
|
| 19 |
+
parts.append(UTR5Part(
|
| 20 |
+
name="Ξ²-globin_5UTR",
|
| 21 |
+
sequence="GTTGCTCCTTCGGGCTGCTGGGGTGAATAGTTCTGCCTTCGCGGCGCCGCCAAGTCCT",
|
| 22 |
+
source="library",
|
| 23 |
+
))
|
| 24 |
+
|
| 25 |
+
# CMV 5'UTR - very strong for mammalian expression
|
| 26 |
+
parts.append(UTR5Part(
|
| 27 |
+
name="CMV_5UTR",
|
| 28 |
+
sequence="GACATTGATTATTGACTAGTTATTAATAGTAATCAATTACGGGGTCATTAGTTCATAGCCCATATATGGAGTTCCGCGTTACATAACTTACGGTAAATGGCCCGCCTGGCTGACCGCCCAACGACCCCCGCCCATTGACGTCAATAATGACGTATGTTCCCATAGTAACGCCAATAGGGACTTTCCATTGACGTC",
|
| 29 |
+
source="library",
|
| 30 |
+
))
|
| 31 |
+
|
| 32 |
+
# HBB 5'UTR - hemoglobin beta
|
| 33 |
+
parts.append(UTR5Part(
|
| 34 |
+
name="HBB_5UTR",
|
| 35 |
+
sequence="ACATTTGCTTCTGACACAACTGTGTTCACTAGCAACCTCAAACAGACACCATGGTGCAT",
|
| 36 |
+
source="library",
|
| 37 |
+
))
|
| 38 |
+
|
| 39 |
+
# GAPDH 5'UTR - housekeeping gene
|
| 40 |
+
parts.append(UTR5Part(
|
| 41 |
+
name="GAPDH_5UTR",
|
| 42 |
+
sequence="CGCTCTCTGCTCCTCCTGTTCGACAGTCAGCCGCATCTTCTTTTGCGTCGCCAGCCGAGCCACATCGCTC",
|
| 43 |
+
source="library",
|
| 44 |
+
))
|
| 45 |
+
|
| 46 |
+
# Minimal synthetic 5'UTR - very short, minimal structure
|
| 47 |
+
parts.append(UTR5Part(
|
| 48 |
+
name="Minimal_5UTR",
|
| 49 |
+
sequence="GGGAAATAAGAGAGAAAAGAAGAGTAAGAAGAAATATAAGAGCCACC",
|
| 50 |
+
source="library",
|
| 51 |
+
))
|
| 52 |
+
|
| 53 |
+
# ββ Kozak sequences βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 54 |
+
|
| 55 |
+
# Optimal Kozak consensus
|
| 56 |
+
parts.append(KozakPart(
|
| 57 |
+
name="Kozak_Optimal",
|
| 58 |
+
sequence="GCCACCATG",
|
| 59 |
+
source="library",
|
| 60 |
+
consensus_score=1.0,
|
| 61 |
+
matches_canonical=True,
|
| 62 |
+
))
|
| 63 |
+
|
| 64 |
+
# Strong Kozak variant
|
| 65 |
+
parts.append(KozakPart(
|
| 66 |
+
name="Kozak_Strong",
|
| 67 |
+
sequence="GCCGCCATG",
|
| 68 |
+
source="library",
|
| 69 |
+
consensus_score=0.95,
|
| 70 |
+
matches_canonical=True,
|
| 71 |
+
))
|
| 72 |
+
|
| 73 |
+
# Moderate Kozak
|
| 74 |
+
parts.append(KozakPart(
|
| 75 |
+
name="Kozak_Moderate",
|
| 76 |
+
sequence="ACCATG",
|
| 77 |
+
source="library",
|
| 78 |
+
consensus_score=0.7,
|
| 79 |
+
matches_canonical=False,
|
| 80 |
+
))
|
| 81 |
+
|
| 82 |
+
# ββ CDS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 83 |
+
|
| 84 |
+
# EGFP - Enhanced Green Fluorescent Protein (codon optimized)
|
| 85 |
+
parts.append(CDSPart(
|
| 86 |
+
name="EGFP_CDS",
|
| 87 |
+
sequence="ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCCTGACCTACGGCGTGCAGTGCTTCAGCCGCTACCCCGACCACATGAAGCAGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCACCCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAA",
|
| 88 |
+
source="library",
|
| 89 |
+
has_start_codon=True,
|
| 90 |
+
has_stop_codon=True,
|
| 91 |
+
))
|
| 92 |
+
|
| 93 |
+
# Luciferase - reporter gene
|
| 94 |
+
parts.append(CDSPart(
|
| 95 |
+
name="Luciferase_CDS",
|
| 96 |
+
sequence="ATGGAAGACGCCAAAAACATAAAGAAAGGCCCGGCGCCATTCTATCCGCTGGAAGATGGAACCGCTGGAGAGCAACTGCATAAGGCTATGAAGAGATACGCCCTGGTTCCTGGAACAATTGCTTTTACAGATGCACATATCGAGGTGGACATCACTTACGCTGAGTACTTCGAAATGTCCGTTCGGTTGGCAGAAGCTATGAAACGATATGGGCTGAATACAAATCACAGAATCGTCGTATGCAGTGAAAACTCTCTTCAATTCTTTATGCCGGTGTTGGGCGCGTTATTTATCGGAGTTGCAGTTGCGCCCGCGAACGACATTTATAATGAACGTGAATTGCTCAACAGTATGGGCATTTCGCAGCCTACCGTGGTGTTCGTTTCCAAAAAGGGGTTGCAAAAAATTTTGAACGTGCAAAAAAAGCTCCCAATCATCCAAAAAATTATTATCATGGATTCTAAAACGGATTACCAGGGATTTCAGTCGATGTACACGTTCGTCACATCTCATCTACCTCCCGGTTTTAATGAATACGATTTTGTGCCAGAGTCCTTCGATAGGGACAAGACAATTGCACTGATCATGAACTCCTCTGGATCTACTGGTCTGCCTAAAGGTGTCGCTCTGCCTCATAGAACTGCCTGCGTGAGATTCTCGCATGCCAGAGATCCTATTTTTGGCAATCAAATCATTCCGGATACTGCGATTTTAAGTGTTGTTCCATTCCATCACGGTTTTGGAATGTTTACTACACTCGGATATTTGATATGTGGATTTCGAGTCGTCTTAATGTATAGATTTGAAGAAGAGCTGTTTCTGAGGAGCCTTCAGGATTACAAGATTCAAAGTGCGCTGCTGGTGCCAACCCTATTCTCCTTCTTCGCCAAAAGCACTCTGATTGACAAATACGATTTATCTAATTTACACGAAATTGCTTCTGGTGGCGCTCCCCTCTCTAAGGAAGTCGGGGAAGCGGTTGCCAAGAGGTTCCATCTGCCAGGTATCAGGCAAGGATATGGGCTCACTGAGACTACATCAGCTATTCTGATTACACCCGAGGGGGATGATAAACCGGGCGCGGTCGGTAAAGTTGTTCCATTTTTTGAAGCGAAGGTTGTGGATCTGGATACCGGGAAAACGCTGGGCGTTAATCAAAGAGGCGAACTGTGTGTGAGAGGTCCTATGATTATGTCCGGTTATGTAAACAATCCGGAAGCGACCAACGCCTTGATTGACAAGGATGGATGGCTACATTCTGGAGACATAGCTTACTGGGACGAAGACGAACACTTCTTCATCGTTGACCGCCTGAAGTCTCTGATTAAGTACAAAGGCTATCAGGTGGCTCCCGCTGAATTGGAATCCATCTTGCTCCAACACCCCAACATCTTCGACGCAGGTGTCGCAGGTCTTCCCGACGATGACGCCGGTGAACTTCCCGCCGCCGTTGTTGTTTTGGAGCACGGAAAGACGATGACGGAAAAAGAGATCGTGGATTACGTCGCCAGTCAAGTAACAACCGCGAAAAAGTTGCGCGGAGGAGTTGTGTTTGTGGACGAAGTACCGAAAGGTCTTACCGGAAAACTCGACGCAAGAAAAATCAGAGAGATCCTCATAAAGGCCAAGAAGGGCGGAAAGATCGCCGTGTAA",
|
| 97 |
+
source="library",
|
| 98 |
+
has_start_codon=True,
|
| 99 |
+
has_stop_codon=True,
|
| 100 |
+
))
|
| 101 |
+
|
| 102 |
+
# mCherry - red fluorescent protein
|
| 103 |
+
parts.append(CDSPart(
|
| 104 |
+
name="mCherry_CDS",
|
| 105 |
+
sequence="ATGGTGAGCAAGGGCGAGGAGGATAACATGGCCATCATCAAGGAGTTCATGCGCTTCAAGGTGCACATGGAGGGCTCCGTGAACGGCCACGAGTTCGAGATCGAGGGCGAGGGCGAGGGCCGCCCCTACGAGGGCACCCAGACCGCCAAGCTGAAGGTGACCAAGGGTGGCCCCCTGCCCTTCGCCTGGGACATCCTGTCCCCTCAGTTCATGTACGGCTCCAAGGCCTACGTGAAGCACCCCGCCGACATCCCCGACTACTTGAAGCTGTCCTTCCCCGAGGGCTTCAAGTGGGAGCGCGTGATGAACTTCGAGGACGGCGGCGTGGTGACCGTGACCCAGGACTCCTCCCTGCAGGACGGCGAGTTCATCTACAAGGTGAAGCTGCGCGGCACCAACTTCCCCTCCGACGGCCCCGTAATGCAGAAGAAGACCATGGGCTGGGAGGCCTCCTCCGAGCGGATGTACCCCGAGGACGGCGCCCTGAAGGGCGAGATCAAGCAGAGGCTGAAGCTGAAGGACGGCGGCCACTACGACGCTGAGGTCAAGACCACCTACAAGGCCAAGAAGCCCGTGCAGCTGCCCGGCGCCTACAACGTCAACATCAAGTTGGACATCACCTCCCACAACGAGGACTACACCATCGTGGAACAGTACGAACGCGCCGAGGGCCGCCACTCCACCGGCGGCATGGACGAGCTGTACAAGTAA",
|
| 106 |
+
source="library",
|
| 107 |
+
has_start_codon=True,
|
| 108 |
+
has_stop_codon=True,
|
| 109 |
+
))
|
| 110 |
+
|
| 111 |
+
# Short peptide tag - His6 tag
|
| 112 |
+
parts.append(CDSPart(
|
| 113 |
+
name="His6_Tag",
|
| 114 |
+
sequence="ATGCATCATCATCATCATCATTAA",
|
| 115 |
+
source="library",
|
| 116 |
+
has_start_codon=True,
|
| 117 |
+
has_stop_codon=True,
|
| 118 |
+
))
|
| 119 |
+
|
| 120 |
+
# ββ 3' UTRs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 121 |
+
|
| 122 |
+
# Ξ²-globin 3'UTR - very stable
|
| 123 |
+
parts.append(UTR3Part(
|
| 124 |
+
name="Ξ²-globin_3UTR",
|
| 125 |
+
sequence="CTGACTTTCTGCCTTTCCCTGGGCTTTTGCTACCCTTGTTGCCTTTTGGCGTCTTCCTTGCCTTCCTTCTGGTTGGTTTTTCCTCTTGTTGATGCTATGGATCCGCTGCATTAATGAATCGGCCAACGCGCGGGGAGAGGCGGTTTGCGTATTGGGCGCTCTTCCGCTTCCTCGCTCACTGACTCGCTGCGCTCGGTCGTTCGGCTGCGGCGAGCGGTATCAGCTCACTCAAAGGCGGTAATACGGTTATCCACAGAATCAGGGGATAACGCAGGAAAGAACATGTGAGCAAAAGGCCAGCAAAAGGCCAGGAACCGTAAAAAGGCCGCGTTGCTGGCGTTTTTCCATAGGCTCCGCCCCCCTGACGAGCATCACAAAAATCGACGCTCAAGTCAGAGGTGGCGAAACCCGACAGGACTATAAAGATACCAGGCGTTTCCCCCTGGAAGCTCCCTCGTGCGCTCTCCTGTTCCGACCCTGCCGCTTACCGGATACCTGTCCGCCTTTCTCCCTTCGGGAAGCGTGGCGCTTTCTCATAGCTCACGCTGTAGGTATCTCAGTTCGGTGTAGGTCGTTCGCTCCAAGCTGGGCTGTGTGCACGAACCCCCCGTTCAGCCCGACCGCTGCGCCTTATCCGGTAACTATCGTCTTGAGTCCAACCCGGTAAGACACGACTTATCGCCACTGGCAGCAGCCACTGGTAACAGGATTAGCAGAGCGAGGTATGTAGGCGGTGCTACAGAGTTCTTGAAGTGGTGGCCTAACTACGGCTACACTAGAAGAACAGTATTTGGTATCTGCGCTCTGCTGAAGCCAGTTACCTTCGGAAAAAGAGTTGGTAGCTCTTGATCCGGCAAACAAACCACCGCTGGTAGCGGTGGTTTTTTTGTTTGCAAGCAGCAGATTACGCGCAGAAAAAAAGGATCTCAAGAAGATCCTTTGATCTTTTCTACGGGGTCTGACGCTCAGTGGAACGAAAACTCACGTTAAGGGATTTTGGTCATGAGATTATCAAAAAGGATCTTCACCTAGATCCTTTTAAATTAAAAATGAAGTTTTAAATCAATCTAAAGTATATATGAGTAAACTTGGTCTGACAGTTACCAATGCTTAATCAGTGAGGCACCTATCTCAGCGATCTGTCTATTTCGTTCATCCATAGTTGCCTGACTCCCCGTCGTGTAGATAACTACGATACGGGAGGGCTTACCATCTGGCCCCAGTGCTGCAATGATACCGCGAGACCCACGCTCACCGGCTCCAGATTTATCAGCAATAAACCAGCCAGCCGGAAGGGCCGAGCGCAGAAGTGGTCCTGCAACTTTATCCGCCTCCATCCAGTCTATTAATTGTTGCCGGGAAGCTAGAGTAAGTAGTTCGCCAGTTAATAGTTTGCGCAACGTTGTTGCCATTGCTACAGGCATCGTGGTGTCACGCTCGTCGTTTGGTATGGCTTCATTCAGCTCCGGTTCCCAACGATCAAGGCGAGTTACATGATCCCCCATGTTGTGCAAAAAAGCGGTTAGCTCCTTCGGTCCTCCGATCGTTGTCAGAAGTAAGTTGGCCGCAGTGTTATCACTCATGGTTATGGCAGCACTGCATAATTCTCTTACTGTCATGCCATCCGTAAGATGCTTTTCTGTGACTGGTGAGTACTCAACCAAGTCATTCTGAGAATAGTGTATGCGGCGACCGAGTTGCTCTTGCCCGGCGTCAATACGGGATAATACCGCGCCACATAGCAGAACTTTAAAAGTGCTCATCATTGGAAAACGTTCTTCGGGGCGAAAACTCTCAAGGATCTTACCGCTGTTGAGATCCAGTTCGATGTAACCCACTCGTGCACCCAACTGATCTTCAGCATCTTTTACTTTCACCAGCGTTTCTGGGTGAGCAAAAACAGGAAGGCAAAATGCCGCAAAAAAGGGAATAAGGGCGACACGGAAATGTTGAATACTCATACTCTTCCTTTTTCAATATTATTGAAGCATTTATCAGGGTTATTGTCTCATGAGCGGATACATATTTGAATGTATTTAGAAAAATAAACAAATAGGGGTTCCGCGCACATTTCCCCGAAAAGTGCCACCTGACGTC",
|
| 126 |
+
source="library",
|
| 127 |
+
))
|
| 128 |
+
|
| 129 |
+
# SV40 polyA signal (short)
|
| 130 |
+
parts.append(UTR3Part(
|
| 131 |
+
name="SV40_3UTR",
|
| 132 |
+
sequence="GATCCAGACATGATAAGATACATTGATGAGTTTGGACAAACCACAACTAGAATGCAGTGAAAAAAATGCTTTATTTGTGAAATTTGTGATGCTATTGCTTTATTTGTAACCATTATAAGCTGCAATAAACAAGTTAACAACAACAATTGCATTCATTTTATGTTTCAGGTTCAGGGGGAGGTGTGGGAGGTTTTTTAAAGCAAGTAAAACCTCTACAAATGTGGTA",
|
| 133 |
+
source="library",
|
| 134 |
+
))
|
| 135 |
+
|
| 136 |
+
# Human Growth Hormone 3'UTR
|
| 137 |
+
parts.append(UTR3Part(
|
| 138 |
+
name="hGH_3UTR",
|
| 139 |
+
sequence="CTCGAGGTCGACGGTATCGATAAGCTTGATATCGAATTCCTGCAGCCCGGGGGATCCACTAGTTCTAGAGCGGCCGCCACCGCGGTGGAGCTCCAGCTTTTGTTCCCTTTAGTGAGGGTTAATTGCGCGCTTGGCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCACAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGCCTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTTCCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGCGGGGAGAGGCGGTTTGCGTATTGGGCGCTCTTCCGCTTCCTCGCTCACTGACTCGCTGCGCTCGGTCGTTCGGCTGCGGCGAGCGGTATCAGCTCACTCAAAGGCGGTAATACGGTTATCCACAGAATCAGGGGATAACGCAGGAAAGAACATGTGAGCAAAAGGCCAGCAAAAGGCCAGGAACCGTAAAAAGGCCGCGTTGCTGGCGTTTTTCCATAGGCTCCGCCCCCCTGACGAGCATCACAAAAATCGACGCTCAAGTCAGAGGTGGCGAAACCCGACAGGACTATAAAGATACCAGGCGTTTCCCCCTGGAAGCTCCCTCGTGCGCTCTCCTGTTCCGACCCTGCCGCTTACCGGATACCTGTCCGCCTTTCTCCCTTCGGGAAGCGTGGCGCTTTCTCATAGCTCACGCTGTAGGTATCTCAGTTCGGTGTAGGTCGTTCGCTCCAAGCTGGGCTGTGTGCACGAACCCCCCGTTCAGCCCGACCGCTGCGCCTTATCCGGTAACTATCGTCTTGAGTCCAACCCGGTAAGACACGACTTATCGCCACTGG",
|
| 140 |
+
source="library",
|
| 141 |
+
))
|
| 142 |
+
|
| 143 |
+
# Albumin 3'UTR
|
| 144 |
+
parts.append(UTR3Part(
|
| 145 |
+
name="Albumin_3UTR",
|
| 146 |
+
sequence="TGCCTGGCACTTGTCCCGAACAGTTGGGGGAGGGGAGGGAGTAGGTTGTGTGTGTTGGG",
|
| 147 |
+
source="library",
|
| 148 |
+
))
|
| 149 |
+
|
| 150 |
+
# ββ Poly(A) tails βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 151 |
+
|
| 152 |
+
# Standard 30A tail
|
| 153 |
+
parts.append(PolyAPart(
|
| 154 |
+
name="PolyA_30",
|
| 155 |
+
sequence="A" * 30,
|
| 156 |
+
source="library",
|
| 157 |
+
tail_length=30,
|
| 158 |
+
purity=1.0,
|
| 159 |
+
))
|
| 160 |
+
|
| 161 |
+
# Long 60A tail
|
| 162 |
+
parts.append(PolyAPart(
|
| 163 |
+
name="PolyA_60",
|
| 164 |
+
sequence="A" * 60,
|
| 165 |
+
source="library",
|
| 166 |
+
tail_length=60,
|
| 167 |
+
purity=1.0,
|
| 168 |
+
))
|
| 169 |
+
|
| 170 |
+
# Standard 120A tail (typical for mRNA)
|
| 171 |
+
parts.append(PolyAPart(
|
| 172 |
+
name="PolyA_120",
|
| 173 |
+
sequence="A" * 120,
|
| 174 |
+
source="library",
|
| 175 |
+
tail_length=120,
|
| 176 |
+
purity=1.0,
|
| 177 |
+
))
|
| 178 |
+
|
| 179 |
+
# Short 15A tail
|
| 180 |
+
parts.append(PolyAPart(
|
| 181 |
+
name="PolyA_15",
|
| 182 |
+
sequence="A" * 15,
|
| 183 |
+
source="library",
|
| 184 |
+
tail_length=15,
|
| 185 |
+
purity=1.0,
|
| 186 |
+
))
|
| 187 |
+
|
| 188 |
+
# Mixed tail (A-rich but not pure)
|
| 189 |
+
parts.append(PolyAPart(
|
| 190 |
+
name="PolyA_Mixed_50",
|
| 191 |
+
sequence="AAAAAAAAACAAAAAAAAAACAAAAAAAAAACAAAAAAAAAACAAAAAAAAAA",
|
| 192 |
+
source="library",
|
| 193 |
+
tail_length=50,
|
| 194 |
+
purity=0.94,
|
| 195 |
+
))
|
| 196 |
+
|
| 197 |
+
return parts
|
core/database/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Database connectors and schema mapper."""
|
| 2 |
+
from core.database.base import (
|
| 3 |
+
ConnectionConfig,
|
| 4 |
+
DatabaseConnector,
|
| 5 |
+
FieldMapping,
|
| 6 |
+
SchemaMapper,
|
| 7 |
+
SEQUENCE_FIELDS,
|
| 8 |
+
)
|
| 9 |
+
from core.database.sqlite import SQLiteConnector
|
| 10 |
+
from core.database.postgres import PostgreSQLConnector
|
| 11 |
+
from core.database.csv_importer import CSVConnector
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def create_connector(config: ConnectionConfig) -> DatabaseConnector:
|
| 15 |
+
"""Factory: return the appropriate connector for config.backend."""
|
| 16 |
+
backends = {
|
| 17 |
+
"sqlite": SQLiteConnector,
|
| 18 |
+
"postgres": PostgreSQLConnector,
|
| 19 |
+
"postgresql": PostgreSQLConnector,
|
| 20 |
+
"csv": CSVConnector,
|
| 21 |
+
"excel": CSVConnector,
|
| 22 |
+
}
|
| 23 |
+
cls = backends.get(config.backend.lower())
|
| 24 |
+
if cls is None:
|
| 25 |
+
raise ValueError(
|
| 26 |
+
f"Unknown backend '{config.backend}'. "
|
| 27 |
+
f"Supported: {list(backends.keys())}"
|
| 28 |
+
)
|
| 29 |
+
return cls(config)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
__all__ = [
|
| 33 |
+
"ConnectionConfig",
|
| 34 |
+
"DatabaseConnector",
|
| 35 |
+
"FieldMapping",
|
| 36 |
+
"SchemaMapper",
|
| 37 |
+
"SEQUENCE_FIELDS",
|
| 38 |
+
"SQLiteConnector",
|
| 39 |
+
"PostgreSQLConnector",
|
| 40 |
+
"CSVConnector",
|
| 41 |
+
"create_connector",
|
| 42 |
+
]
|
core/database/base.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Abstract database connector interface and SchemaMapper.
|
| 3 |
+
|
| 4 |
+
Every database backend (SQLite, PostgreSQL, CSV) implements DatabaseConnector.
|
| 5 |
+
SchemaMapper translates arbitrary column names to the mRNASequence model fields.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from abc import ABC, abstractmethod
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from typing import Any, Dict, List, Optional
|
| 12 |
+
|
| 13 |
+
import pandas as pd
|
| 14 |
+
|
| 15 |
+
from core.models.sequence import mRNASequence
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# Fields in mRNASequence that can be mapped from a database
|
| 19 |
+
SEQUENCE_FIELDS = {
|
| 20 |
+
"name",
|
| 21 |
+
"five_prime_utr",
|
| 22 |
+
"kozak",
|
| 23 |
+
"cds",
|
| 24 |
+
"three_prime_utr",
|
| 25 |
+
"poly_a",
|
| 26 |
+
"full_mrna",
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class ConnectionConfig:
|
| 32 |
+
"""Generic connection configuration (fields vary by backend)."""
|
| 33 |
+
backend: str # "sqlite", "postgres", "csv", "excel"
|
| 34 |
+
display_name: str # User-facing label for the connection
|
| 35 |
+
params: Dict[str, Any] = field(default_factory=dict)
|
| 36 |
+
# e.g. sqlite: {"path": "/data/seqs.db"}
|
| 37 |
+
# e.g. postgres: {"host": "...", "port": 5432, "dbname": "...", "user": "...", "password": "..."}
|
| 38 |
+
# e.g. csv: {"path": "/data/seqs.csv"}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class DatabaseConnector(ABC):
|
| 42 |
+
"""Abstract database connector. One instance per active connection."""
|
| 43 |
+
|
| 44 |
+
def __init__(self, config: ConnectionConfig) -> None:
|
| 45 |
+
self.config = config
|
| 46 |
+
self._connected = False
|
| 47 |
+
|
| 48 |
+
@abstractmethod
|
| 49 |
+
def connect(self) -> None:
|
| 50 |
+
"""Open the connection. Raises ConnectionError on failure."""
|
| 51 |
+
...
|
| 52 |
+
|
| 53 |
+
@abstractmethod
|
| 54 |
+
def disconnect(self) -> None:
|
| 55 |
+
"""Close the connection."""
|
| 56 |
+
...
|
| 57 |
+
|
| 58 |
+
@abstractmethod
|
| 59 |
+
def list_tables(self) -> List[str]:
|
| 60 |
+
"""Return available table / sheet names."""
|
| 61 |
+
...
|
| 62 |
+
|
| 63 |
+
@abstractmethod
|
| 64 |
+
def get_records(
|
| 65 |
+
self,
|
| 66 |
+
table: str,
|
| 67 |
+
query: Optional[str] = None,
|
| 68 |
+
limit: Optional[int] = None,
|
| 69 |
+
) -> pd.DataFrame:
|
| 70 |
+
"""
|
| 71 |
+
Fetch records from a table.
|
| 72 |
+
|
| 73 |
+
Parameters
|
| 74 |
+
----------
|
| 75 |
+
table : str
|
| 76 |
+
Table name (from list_tables).
|
| 77 |
+
query : str, optional
|
| 78 |
+
Backend-specific filter string (SQL WHERE clause for SQL backends,
|
| 79 |
+
pandas query string for file backends).
|
| 80 |
+
limit : int, optional
|
| 81 |
+
Max rows to return.
|
| 82 |
+
"""
|
| 83 |
+
...
|
| 84 |
+
|
| 85 |
+
@abstractmethod
|
| 86 |
+
def get_columns(self, table: str) -> List[str]:
|
| 87 |
+
"""Return column names for a table."""
|
| 88 |
+
...
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def is_connected(self) -> bool:
|
| 92 |
+
return self._connected
|
| 93 |
+
|
| 94 |
+
@property
|
| 95 |
+
def name(self) -> str:
|
| 96 |
+
return self.config.display_name
|
| 97 |
+
|
| 98 |
+
def __repr__(self) -> str:
|
| 99 |
+
status = "connected" if self._connected else "disconnected"
|
| 100 |
+
return f"{self.__class__.__name__}({self.name!r}, {status})"
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# ββ Schema Mapper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 104 |
+
|
| 105 |
+
@dataclass
|
| 106 |
+
class FieldMapping:
|
| 107 |
+
"""
|
| 108 |
+
Describes how one database column maps to a mRNASequence field.
|
| 109 |
+
|
| 110 |
+
source_column : str
|
| 111 |
+
Column name in the database.
|
| 112 |
+
target_field : str
|
| 113 |
+
Field name in mRNASequence. Must be in SEQUENCE_FIELDS.
|
| 114 |
+
transform : callable, optional
|
| 115 |
+
Optional transform applied to the raw value before assignment.
|
| 116 |
+
E.g. str.upper, lambda x: x.replace(" ", "")
|
| 117 |
+
"""
|
| 118 |
+
source_column: str
|
| 119 |
+
target_field: str
|
| 120 |
+
transform: Optional[Any] = None # callable or None
|
| 121 |
+
|
| 122 |
+
def __post_init__(self) -> None:
|
| 123 |
+
if self.target_field not in SEQUENCE_FIELDS:
|
| 124 |
+
raise ValueError(
|
| 125 |
+
f"'{self.target_field}' is not a valid mRNASequence field. "
|
| 126 |
+
f"Valid fields: {sorted(SEQUENCE_FIELDS)}"
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class SchemaMapper:
|
| 131 |
+
"""
|
| 132 |
+
Maps a DataFrame (from any DatabaseConnector) to a list of mRNASequence
|
| 133 |
+
objects using a user-configured field mapping.
|
| 134 |
+
|
| 135 |
+
Example
|
| 136 |
+
-------
|
| 137 |
+
mapper = SchemaMapper([
|
| 138 |
+
FieldMapping("mrna_sequence", "full_mrna"),
|
| 139 |
+
FieldMapping("gene_name", "name"),
|
| 140 |
+
FieldMapping("utr5_sequence", "five_prime_utr", transform=str.upper),
|
| 141 |
+
])
|
| 142 |
+
sequences = mapper.map_dataframe(df, db_source="my_lims")
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
def __init__(self, mappings: List[FieldMapping], db_source: str = "") -> None:
|
| 146 |
+
self.mappings = mappings
|
| 147 |
+
self.db_source = db_source
|
| 148 |
+
# Validate: exactly one mapping targeting 'name' must exist
|
| 149 |
+
name_targets = [m for m in mappings if m.target_field == "name"]
|
| 150 |
+
if not name_targets:
|
| 151 |
+
raise ValueError(
|
| 152 |
+
"SchemaMapper requires at least one FieldMapping targeting 'name'."
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
def map_row(self, row: Dict[str, Any]) -> mRNASequence:
|
| 156 |
+
"""Map a single row dict to an mRNASequence."""
|
| 157 |
+
kwargs: Dict[str, Any] = {
|
| 158 |
+
"source": "database",
|
| 159 |
+
"db_source": self.db_source,
|
| 160 |
+
"raw_metadata": dict(row),
|
| 161 |
+
}
|
| 162 |
+
for mapping in self.mappings:
|
| 163 |
+
value = row.get(mapping.source_column)
|
| 164 |
+
# Skip None and NaN values (pandas often returns NaN for SQL NULL)
|
| 165 |
+
if value is None or (isinstance(value, float) and pd.isna(value)):
|
| 166 |
+
continue
|
| 167 |
+
if mapping.transform is not None:
|
| 168 |
+
try:
|
| 169 |
+
value = mapping.transform(value)
|
| 170 |
+
except Exception:
|
| 171 |
+
pass
|
| 172 |
+
kwargs[mapping.target_field] = value
|
| 173 |
+
# name is required β fall back to first non-empty string value in the row
|
| 174 |
+
if "name" not in kwargs or not kwargs["name"]:
|
| 175 |
+
for v in row.values():
|
| 176 |
+
if isinstance(v, str) and v.strip():
|
| 177 |
+
kwargs["name"] = v.strip()[:80]
|
| 178 |
+
break
|
| 179 |
+
else:
|
| 180 |
+
kwargs["name"] = "unnamed"
|
| 181 |
+
return mRNASequence(**kwargs) # type: ignore[arg-type]
|
| 182 |
+
|
| 183 |
+
def map_dataframe(self, df: pd.DataFrame) -> List[mRNASequence]:
|
| 184 |
+
"""Map every row in df to an mRNASequence."""
|
| 185 |
+
return [self.map_row(row.to_dict()) for _, row in df.iterrows()]
|
| 186 |
+
|
| 187 |
+
@classmethod
|
| 188 |
+
def from_dict(cls, mapping_dict: Dict[str, str], db_source: str = "") -> "SchemaMapper":
|
| 189 |
+
"""
|
| 190 |
+
Convenience constructor from a plain {db_column: sequence_field} dict.
|
| 191 |
+
|
| 192 |
+
Example
|
| 193 |
+
-------
|
| 194 |
+
mapper = SchemaMapper.from_dict({
|
| 195 |
+
"gene_name": "name",
|
| 196 |
+
"mrna_seq": "full_mrna",
|
| 197 |
+
"utr": "five_prime_utr",
|
| 198 |
+
})
|
| 199 |
+
"""
|
| 200 |
+
mappings = [
|
| 201 |
+
FieldMapping(source_column=col, target_field=field_)
|
| 202 |
+
for col, field_ in mapping_dict.items()
|
| 203 |
+
]
|
| 204 |
+
return cls(mappings, db_source=db_source)
|
core/database/csv_importer.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CSV and Excel flat-file connector."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
|
| 9 |
+
from core.database.base import ConnectionConfig, DatabaseConnector
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class CSVConnector(DatabaseConnector):
|
| 13 |
+
"""
|
| 14 |
+
Connector for CSV and Excel flat files.
|
| 15 |
+
|
| 16 |
+
For CSV: treats the single file as one 'table' named by the filename stem.
|
| 17 |
+
For Excel: each worksheet is a 'table'.
|
| 18 |
+
A directory of CSV files is also supported β each file becomes a table.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, config: ConnectionConfig) -> None:
|
| 22 |
+
super().__init__(config)
|
| 23 |
+
self._dataframes: Dict[str, pd.DataFrame] = {}
|
| 24 |
+
|
| 25 |
+
def connect(self) -> None:
|
| 26 |
+
path_str = self.config.params.get("path")
|
| 27 |
+
if not path_str:
|
| 28 |
+
raise ValueError("CSV/Excel config must include 'path'.")
|
| 29 |
+
path = Path(path_str)
|
| 30 |
+
if not path.exists():
|
| 31 |
+
raise FileNotFoundError(f"File not found: {path}")
|
| 32 |
+
|
| 33 |
+
self._dataframes = {}
|
| 34 |
+
|
| 35 |
+
if path.is_dir():
|
| 36 |
+
# Load all CSVs in directory
|
| 37 |
+
for csv_file in sorted(path.glob("*.csv")):
|
| 38 |
+
df = pd.read_csv(csv_file)
|
| 39 |
+
self._dataframes[csv_file.stem] = df
|
| 40 |
+
if not self._dataframes:
|
| 41 |
+
raise ValueError(f"No CSV files found in directory: {path}")
|
| 42 |
+
|
| 43 |
+
elif path.suffix.lower() in (".xlsx", ".xls"):
|
| 44 |
+
xl = pd.ExcelFile(path)
|
| 45 |
+
for sheet in xl.sheet_names:
|
| 46 |
+
self._dataframes[sheet] = xl.parse(sheet)
|
| 47 |
+
|
| 48 |
+
elif path.suffix.lower() == ".csv":
|
| 49 |
+
df = pd.read_csv(path)
|
| 50 |
+
self._dataframes[path.stem] = df
|
| 51 |
+
|
| 52 |
+
else:
|
| 53 |
+
raise ValueError(
|
| 54 |
+
f"Unsupported file type: {path.suffix}. Use .csv, .xlsx, or .xls."
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
self._connected = True
|
| 58 |
+
|
| 59 |
+
def disconnect(self) -> None:
|
| 60 |
+
self._dataframes.clear()
|
| 61 |
+
self._connected = False
|
| 62 |
+
|
| 63 |
+
def list_tables(self) -> List[str]:
|
| 64 |
+
return list(self._dataframes.keys())
|
| 65 |
+
|
| 66 |
+
def get_columns(self, table: str) -> List[str]:
|
| 67 |
+
self._require_connected()
|
| 68 |
+
df = self._get_table(table)
|
| 69 |
+
return list(df.columns)
|
| 70 |
+
|
| 71 |
+
def get_records(
|
| 72 |
+
self,
|
| 73 |
+
table: str,
|
| 74 |
+
query: Optional[str] = None,
|
| 75 |
+
limit: Optional[int] = None,
|
| 76 |
+
) -> pd.DataFrame:
|
| 77 |
+
self._require_connected()
|
| 78 |
+
df = self._get_table(table).copy()
|
| 79 |
+
if query:
|
| 80 |
+
try:
|
| 81 |
+
df = df.query(query)
|
| 82 |
+
except Exception as e:
|
| 83 |
+
raise ValueError(f"Query error: {e}") from e
|
| 84 |
+
if limit:
|
| 85 |
+
df = df.head(limit)
|
| 86 |
+
return df.reset_index(drop=True)
|
| 87 |
+
|
| 88 |
+
def _get_table(self, table: str) -> pd.DataFrame:
|
| 89 |
+
if table not in self._dataframes:
|
| 90 |
+
raise KeyError(
|
| 91 |
+
f"Table '{table}' not found. Available: {self.list_tables()}"
|
| 92 |
+
)
|
| 93 |
+
return self._dataframes[table]
|
| 94 |
+
|
| 95 |
+
def _require_connected(self) -> None:
|
| 96 |
+
if not self._connected:
|
| 97 |
+
raise RuntimeError("Not connected. Call connect() first.")
|
core/database/postgres.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PostgreSQL database connector (via SQLAlchemy + psycopg2)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
from sqlalchemy import create_engine, inspect, text
|
| 8 |
+
from sqlalchemy.engine import Engine
|
| 9 |
+
|
| 10 |
+
from core.database.base import ConnectionConfig, DatabaseConnector
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class PostgreSQLConnector(DatabaseConnector):
|
| 14 |
+
"""Connects to a PostgreSQL database."""
|
| 15 |
+
|
| 16 |
+
def __init__(self, config: ConnectionConfig) -> None:
|
| 17 |
+
super().__init__(config)
|
| 18 |
+
self._engine: Optional[Engine] = None
|
| 19 |
+
|
| 20 |
+
def _build_url(self) -> str:
|
| 21 |
+
p = self.config.params
|
| 22 |
+
user = p.get("user", "")
|
| 23 |
+
password = p.get("password", "")
|
| 24 |
+
host = p.get("host", "localhost")
|
| 25 |
+
port = p.get("port", 5432)
|
| 26 |
+
dbname = p.get("dbname", "")
|
| 27 |
+
if password:
|
| 28 |
+
return f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{dbname}"
|
| 29 |
+
return f"postgresql+psycopg2://{user}@{host}:{port}/{dbname}"
|
| 30 |
+
|
| 31 |
+
def connect(self) -> None:
|
| 32 |
+
try:
|
| 33 |
+
url = self._build_url()
|
| 34 |
+
self._engine = create_engine(url, pool_pre_ping=True)
|
| 35 |
+
# Test the connection
|
| 36 |
+
with self._engine.connect() as conn:
|
| 37 |
+
conn.execute(text("SELECT 1"))
|
| 38 |
+
self._connected = True
|
| 39 |
+
except Exception as e:
|
| 40 |
+
raise ConnectionError(f"PostgreSQL connection failed: {e}") from e
|
| 41 |
+
|
| 42 |
+
def disconnect(self) -> None:
|
| 43 |
+
if self._engine:
|
| 44 |
+
self._engine.dispose()
|
| 45 |
+
self._engine = None
|
| 46 |
+
self._connected = False
|
| 47 |
+
|
| 48 |
+
def list_tables(self) -> List[str]:
|
| 49 |
+
self._require_connected()
|
| 50 |
+
inspector = inspect(self._engine)
|
| 51 |
+
return inspector.get_table_names()
|
| 52 |
+
|
| 53 |
+
def get_columns(self, table: str) -> List[str]:
|
| 54 |
+
self._require_connected()
|
| 55 |
+
inspector = inspect(self._engine)
|
| 56 |
+
return [col["name"] for col in inspector.get_columns(table)]
|
| 57 |
+
|
| 58 |
+
def get_records(
|
| 59 |
+
self,
|
| 60 |
+
table: str,
|
| 61 |
+
query: Optional[str] = None,
|
| 62 |
+
limit: Optional[int] = None,
|
| 63 |
+
) -> pd.DataFrame:
|
| 64 |
+
self._require_connected()
|
| 65 |
+
sql = f'SELECT * FROM "{table}"'
|
| 66 |
+
if query:
|
| 67 |
+
sql += f" WHERE {query}"
|
| 68 |
+
if limit:
|
| 69 |
+
sql += f" LIMIT {limit}"
|
| 70 |
+
with self._engine.connect() as conn: # type: ignore[union-attr]
|
| 71 |
+
return pd.read_sql_query(text(sql), conn)
|
| 72 |
+
|
| 73 |
+
def execute_raw(self, sql: str) -> pd.DataFrame:
|
| 74 |
+
"""Run arbitrary read-only SQL."""
|
| 75 |
+
self._require_connected()
|
| 76 |
+
with self._engine.connect() as conn: # type: ignore[union-attr]
|
| 77 |
+
return pd.read_sql_query(text(sql), conn)
|
| 78 |
+
|
| 79 |
+
def _require_connected(self) -> None:
|
| 80 |
+
if not self._connected or self._engine is None:
|
| 81 |
+
raise RuntimeError("Not connected. Call connect() first.")
|
core/database/sqlite.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SQLite database connector."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import sqlite3
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
|
| 9 |
+
from core.database.base import ConnectionConfig, DatabaseConnector
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class SQLiteConnector(DatabaseConnector):
|
| 13 |
+
"""Connects to a local SQLite database file."""
|
| 14 |
+
|
| 15 |
+
def __init__(self, config: ConnectionConfig) -> None:
|
| 16 |
+
super().__init__(config)
|
| 17 |
+
self._conn: Optional[sqlite3.Connection] = None
|
| 18 |
+
|
| 19 |
+
def connect(self) -> None:
|
| 20 |
+
path = self.config.params.get("path")
|
| 21 |
+
if not path:
|
| 22 |
+
raise ValueError("SQLite config must include 'path'.")
|
| 23 |
+
try:
|
| 24 |
+
self._conn = sqlite3.connect(path, check_same_thread=False)
|
| 25 |
+
self._connected = True
|
| 26 |
+
except sqlite3.Error as e:
|
| 27 |
+
raise ConnectionError(f"SQLite connection failed: {e}") from e
|
| 28 |
+
|
| 29 |
+
def disconnect(self) -> None:
|
| 30 |
+
if self._conn:
|
| 31 |
+
self._conn.close()
|
| 32 |
+
self._conn = None
|
| 33 |
+
self._connected = False
|
| 34 |
+
|
| 35 |
+
def list_tables(self) -> List[str]:
|
| 36 |
+
self._require_connected()
|
| 37 |
+
cursor = self._conn.execute( # type: ignore[union-attr]
|
| 38 |
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
|
| 39 |
+
)
|
| 40 |
+
return [row[0] for row in cursor.fetchall()]
|
| 41 |
+
|
| 42 |
+
def get_columns(self, table: str) -> List[str]:
|
| 43 |
+
self._require_connected()
|
| 44 |
+
cursor = self._conn.execute(f'PRAGMA table_info("{table}");') # type: ignore[union-attr]
|
| 45 |
+
return [row[1] for row in cursor.fetchall()]
|
| 46 |
+
|
| 47 |
+
def get_records(
|
| 48 |
+
self,
|
| 49 |
+
table: str,
|
| 50 |
+
query: Optional[str] = None,
|
| 51 |
+
limit: Optional[int] = None,
|
| 52 |
+
) -> pd.DataFrame:
|
| 53 |
+
self._require_connected()
|
| 54 |
+
sql = f'SELECT * FROM "{table}"'
|
| 55 |
+
if query:
|
| 56 |
+
sql += f" WHERE {query}"
|
| 57 |
+
if limit:
|
| 58 |
+
sql += f" LIMIT {limit}"
|
| 59 |
+
return pd.read_sql_query(sql, self._conn) # type: ignore[arg-type]
|
| 60 |
+
|
| 61 |
+
def execute_raw(self, sql: str) -> pd.DataFrame:
|
| 62 |
+
"""Run arbitrary read-only SQL and return a DataFrame."""
|
| 63 |
+
self._require_connected()
|
| 64 |
+
return pd.read_sql_query(sql, self._conn) # type: ignore[arg-type]
|
| 65 |
+
|
| 66 |
+
def _require_connected(self) -> None:
|
| 67 |
+
if not self._connected or self._conn is None:
|
| 68 |
+
raise RuntimeError("Not connected. Call connect() first.")
|
core/models/__init__.py
ADDED
|
File without changes
|
core/models/parts.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Sequence part models for the parts library.
|
| 3 |
+
|
| 4 |
+
SequencePart is the base class; specialized part types inherit to add
|
| 5 |
+
bespoke fields (e.g., CDS has protein_sequence, codon_usage; UTRs have
|
| 6 |
+
stability metrics).
|
| 7 |
+
|
| 8 |
+
Parts are auto-extracted when sequences are analyzed or imported.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import hashlib
|
| 13 |
+
import uuid
|
| 14 |
+
from dataclasses import dataclass, field
|
| 15 |
+
from typing import Any, Dict, Literal, Optional
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class SequencePart:
|
| 20 |
+
"""
|
| 21 |
+
Base class for reusable sequence parts.
|
| 22 |
+
|
| 23 |
+
Each part can be mixed and matched in the Parts Workshop to
|
| 24 |
+
assemble new mRNA sequences.
|
| 25 |
+
"""
|
| 26 |
+
name: str
|
| 27 |
+
sequence: str
|
| 28 |
+
part_type: Literal["5_utr", "kozak", "cds", "3_utr", "polya"]
|
| 29 |
+
source: str # "database_import", "generated", "manual", "extracted"
|
| 30 |
+
|
| 31 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 32 |
+
origin_sequence_id: Optional[str] = None # ID of parent mRNASequence
|
| 33 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def length(self) -> int:
|
| 37 |
+
return len(self.sequence)
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def sequence_hash(self) -> str:
|
| 41 |
+
"""SHA256 hash for deduplication."""
|
| 42 |
+
return hashlib.sha256(self.sequence.encode()).hexdigest()[:16]
|
| 43 |
+
|
| 44 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 45 |
+
return {
|
| 46 |
+
"id": self.id,
|
| 47 |
+
"name": self.name,
|
| 48 |
+
"sequence": self.sequence,
|
| 49 |
+
"part_type": self.part_type,
|
| 50 |
+
"source": self.source,
|
| 51 |
+
"origin_sequence_id": self.origin_sequence_id,
|
| 52 |
+
"metadata": self.metadata,
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@dataclass
|
| 57 |
+
class UTR5Part(SequencePart):
|
| 58 |
+
"""5' UTR part with stability and structure metrics."""
|
| 59 |
+
part_type: Literal["5_utr"] = field(default="5_utr", init=False)
|
| 60 |
+
|
| 61 |
+
# Predicted secondary structure stability
|
| 62 |
+
stability_score: Optional[float] = None
|
| 63 |
+
mfe: Optional[float] = None # Minimum free energy from ViennaRNA
|
| 64 |
+
secondary_structure_dot: Optional[str] = None # Dot-bracket notation
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@dataclass
|
| 68 |
+
class KozakPart(SequencePart):
|
| 69 |
+
"""Kozak consensus sequence part."""
|
| 70 |
+
part_type: Literal["kozak"] = field(default="kozak", init=False)
|
| 71 |
+
|
| 72 |
+
# Consensus score (0-1) based on optimal Kozak pattern
|
| 73 |
+
consensus_score: Optional[float] = None
|
| 74 |
+
# Canonical Kozak is: gcc(A/G)ccATGG
|
| 75 |
+
matches_canonical: Optional[bool] = None
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@dataclass
|
| 79 |
+
class CDSPart(SequencePart):
|
| 80 |
+
"""Coding sequence part with translation and codon metrics."""
|
| 81 |
+
part_type: Literal["cds"] = field(default="cds", init=False)
|
| 82 |
+
|
| 83 |
+
# Translated protein sequence
|
| 84 |
+
protein_sequence: Optional[str] = None
|
| 85 |
+
|
| 86 |
+
# Codon usage metrics
|
| 87 |
+
codon_usage: Optional[Dict[str, float]] = None # {codon: frequency}
|
| 88 |
+
cai: Optional[float] = None # Codon Adaptation Index
|
| 89 |
+
|
| 90 |
+
# Quality checks
|
| 91 |
+
has_start_codon: Optional[bool] = None
|
| 92 |
+
has_stop_codon: Optional[bool] = None
|
| 93 |
+
in_frame: Optional[bool] = None
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@dataclass
|
| 97 |
+
class UTR3Part(SequencePart):
|
| 98 |
+
"""3' UTR part with stability and regulatory element annotations."""
|
| 99 |
+
part_type: Literal["3_utr"] = field(default="3_utr", init=False)
|
| 100 |
+
|
| 101 |
+
# Predicted stability
|
| 102 |
+
stability_score: Optional[float] = None
|
| 103 |
+
mfe: Optional[float] = None
|
| 104 |
+
|
| 105 |
+
# Regulatory elements (AU-rich elements, miRNA binding sites, etc.)
|
| 106 |
+
regulatory_elements: Optional[Dict[str, Any]] = None
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@dataclass
|
| 110 |
+
class PolyAPart(SequencePart):
|
| 111 |
+
"""Poly(A) tail part."""
|
| 112 |
+
part_type: Literal["polya"] = field(default="polya", init=False)
|
| 113 |
+
|
| 114 |
+
# Length of the poly(A) stretch
|
| 115 |
+
tail_length: Optional[int] = None
|
| 116 |
+
|
| 117 |
+
# Purity (percentage of A nucleotides)
|
| 118 |
+
purity: Optional[float] = None
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# Type alias for all part types
|
| 122 |
+
AnyPart = UTR5Part | KozakPart | CDSPart | UTR3Part | PolyAPart
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def create_part_from_component(
|
| 126 |
+
sequence: str,
|
| 127 |
+
part_type: Literal["5_utr", "kozak", "cds", "3_utr", "polya"],
|
| 128 |
+
name: str,
|
| 129 |
+
source: str,
|
| 130 |
+
origin_sequence_id: Optional[str] = None,
|
| 131 |
+
) -> AnyPart:
|
| 132 |
+
"""
|
| 133 |
+
Factory function to create the appropriate Part subclass.
|
| 134 |
+
|
| 135 |
+
Used during auto-extraction from mRNASequence objects.
|
| 136 |
+
"""
|
| 137 |
+
if part_type == "5_utr":
|
| 138 |
+
return UTR5Part(
|
| 139 |
+
name=name,
|
| 140 |
+
sequence=sequence,
|
| 141 |
+
source=source,
|
| 142 |
+
origin_sequence_id=origin_sequence_id,
|
| 143 |
+
)
|
| 144 |
+
elif part_type == "kozak":
|
| 145 |
+
return KozakPart(
|
| 146 |
+
name=name,
|
| 147 |
+
sequence=sequence,
|
| 148 |
+
source=source,
|
| 149 |
+
origin_sequence_id=origin_sequence_id,
|
| 150 |
+
)
|
| 151 |
+
elif part_type == "cds":
|
| 152 |
+
return CDSPart(
|
| 153 |
+
name=name,
|
| 154 |
+
sequence=sequence,
|
| 155 |
+
source=source,
|
| 156 |
+
origin_sequence_id=origin_sequence_id,
|
| 157 |
+
)
|
| 158 |
+
elif part_type == "3_utr":
|
| 159 |
+
return UTR3Part(
|
| 160 |
+
name=name,
|
| 161 |
+
sequence=sequence,
|
| 162 |
+
source=source,
|
| 163 |
+
origin_sequence_id=origin_sequence_id,
|
| 164 |
+
)
|
| 165 |
+
elif part_type == "polya":
|
| 166 |
+
return PolyAPart(
|
| 167 |
+
name=name,
|
| 168 |
+
sequence=sequence,
|
| 169 |
+
source=source,
|
| 170 |
+
origin_sequence_id=origin_sequence_id,
|
| 171 |
+
)
|
| 172 |
+
else:
|
| 173 |
+
raise ValueError(f"Unknown part type: {part_type}")
|
core/models/plasmid.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Plasmid domain models.
|
| 3 |
+
|
| 4 |
+
Covers plasmid backbones and fully assembled plasmids (backbone + mRNA insert).
|
| 5 |
+
Assembly strategies and junction logic live in core/optimization/assembly.py;
|
| 6 |
+
these are pure data structures.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import uuid
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from typing import Any, Dict, List, Literal, Optional, Tuple
|
| 13 |
+
|
| 14 |
+
from core.models.sequence import SequenceAnnotation, mRNASequence
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class PlasmidFeature:
|
| 19 |
+
"""A named functional element within a plasmid sequence."""
|
| 20 |
+
label: str
|
| 21 |
+
feature_type: Literal[
|
| 22 |
+
"promoter", "terminator", "ori", "resistance", "tag",
|
| 23 |
+
"cloning_site", "insert", "utr", "cds", "other"
|
| 24 |
+
]
|
| 25 |
+
start: int # 0-based, within linear representation
|
| 26 |
+
end: int
|
| 27 |
+
strand: Literal["+", "-", "."] = "+"
|
| 28 |
+
color: Optional[str] = None
|
| 29 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 30 |
+
|
| 31 |
+
@property
|
| 32 |
+
def length(self) -> int:
|
| 33 |
+
return self.end - self.start
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass
|
| 37 |
+
class PlasmidBackbone:
|
| 38 |
+
"""
|
| 39 |
+
A cloning vector / expression backbone.
|
| 40 |
+
|
| 41 |
+
The backbone sequence is stored as linearised DNA. For circular
|
| 42 |
+
representation in the UI, the sequence wraps end-to-start.
|
| 43 |
+
cloning_sites lists the restriction enzyme names or recombination
|
| 44 |
+
sites present (for QC/assembly checks).
|
| 45 |
+
"""
|
| 46 |
+
name: str
|
| 47 |
+
sequence: str # full circular backbone, linearised
|
| 48 |
+
|
| 49 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 50 |
+
description: Optional[str] = None
|
| 51 |
+
|
| 52 |
+
# Functional elements (promoters, resistance, ori, MCS, etc.)
|
| 53 |
+
features: List[PlasmidFeature] = field(default_factory=list)
|
| 54 |
+
|
| 55 |
+
# Restriction/recombination sites available for cloning
|
| 56 |
+
cloning_sites: List[str] = field(default_factory=list)
|
| 57 |
+
|
| 58 |
+
# Source: "local", "library", or a db_source name
|
| 59 |
+
source: str = "local"
|
| 60 |
+
raw_metadata: Dict[str, Any] = field(default_factory=dict)
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def length(self) -> int:
|
| 64 |
+
return len(self.sequence)
|
| 65 |
+
|
| 66 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 67 |
+
return {
|
| 68 |
+
"id": self.id,
|
| 69 |
+
"name": self.name,
|
| 70 |
+
"description": self.description,
|
| 71 |
+
"sequence": self.sequence,
|
| 72 |
+
"features": [
|
| 73 |
+
{
|
| 74 |
+
"label": f.label,
|
| 75 |
+
"feature_type": f.feature_type,
|
| 76 |
+
"start": f.start,
|
| 77 |
+
"end": f.end,
|
| 78 |
+
"strand": f.strand,
|
| 79 |
+
"color": f.color,
|
| 80 |
+
}
|
| 81 |
+
for f in self.features
|
| 82 |
+
],
|
| 83 |
+
"cloning_sites": self.cloning_sites,
|
| 84 |
+
"source": self.source,
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
@classmethod
|
| 88 |
+
def from_dict(cls, data: Dict[str, Any]) -> "PlasmidBackbone":
|
| 89 |
+
features = [
|
| 90 |
+
PlasmidFeature(
|
| 91 |
+
label=f["label"],
|
| 92 |
+
feature_type=f.get("feature_type", "other"),
|
| 93 |
+
start=f["start"],
|
| 94 |
+
end=f["end"],
|
| 95 |
+
strand=f.get("strand", "+"),
|
| 96 |
+
color=f.get("color"),
|
| 97 |
+
)
|
| 98 |
+
for f in data.get("features", [])
|
| 99 |
+
]
|
| 100 |
+
return cls(
|
| 101 |
+
id=data.get("id", str(uuid.uuid4())),
|
| 102 |
+
name=data["name"],
|
| 103 |
+
description=data.get("description"),
|
| 104 |
+
sequence=data["sequence"],
|
| 105 |
+
features=features,
|
| 106 |
+
cloning_sites=data.get("cloning_sites", []),
|
| 107 |
+
source=data.get("source", "local"),
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@dataclass
|
| 112 |
+
class AssemblyJunction:
|
| 113 |
+
"""
|
| 114 |
+
Records how two parts are joined in an assembly.
|
| 115 |
+
Stores the junction sequence added/used, and the strategy that created it.
|
| 116 |
+
"""
|
| 117 |
+
part_a_name: str
|
| 118 |
+
part_b_name: str
|
| 119 |
+
strategy: Literal["restriction", "golden_gate", "gibson", "direct"]
|
| 120 |
+
junction_sequence: str # the overhang / overlap / linker added
|
| 121 |
+
enzyme: Optional[str] = None # for restriction/GG assemblies
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@dataclass
|
| 125 |
+
class AssembledPlasmid:
|
| 126 |
+
"""
|
| 127 |
+
A fully assembled plasmid: backbone + mRNA insert, with provenance.
|
| 128 |
+
|
| 129 |
+
full_sequence is the assembled circular sequence as a flat string.
|
| 130 |
+
junctions records every join point for audit / re-assembly.
|
| 131 |
+
In QC mode this is populated from the unmodified parts; in Make mode
|
| 132 |
+
the parts are modified and junctions record what was added.
|
| 133 |
+
"""
|
| 134 |
+
name: str
|
| 135 |
+
backbone: PlasmidBackbone
|
| 136 |
+
insert: mRNASequence
|
| 137 |
+
assembly_strategy: Literal["restriction", "golden_gate", "gibson"]
|
| 138 |
+
assembly_mode: Literal["qc", "make"]
|
| 139 |
+
|
| 140 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 141 |
+
full_sequence: Optional[str] = None # None until assembly is run
|
| 142 |
+
features: List[PlasmidFeature] = field(default_factory=list)
|
| 143 |
+
junctions: List[AssemblyJunction] = field(default_factory=list)
|
| 144 |
+
qc_issues: List[str] = field(default_factory=list) # validation warnings
|
| 145 |
+
notes: Optional[str] = None
|
| 146 |
+
|
| 147 |
+
@property
|
| 148 |
+
def is_assembled(self) -> bool:
|
| 149 |
+
return self.full_sequence is not None
|
| 150 |
+
|
| 151 |
+
@property
|
| 152 |
+
def length(self) -> Optional[int]:
|
| 153 |
+
return len(self.full_sequence) if self.full_sequence else None
|
| 154 |
+
|
| 155 |
+
def to_genbank_annotations(self) -> List[Tuple[str, str, int, int, str]]:
|
| 156 |
+
"""
|
| 157 |
+
Return (label, feature_type, start, end, strand) tuples suitable
|
| 158 |
+
for writing a GenBank file via BioPython.
|
| 159 |
+
"""
|
| 160 |
+
return [
|
| 161 |
+
(f.label, f.feature_type, f.start, f.end, f.strand)
|
| 162 |
+
for f in self.features
|
| 163 |
+
]
|
| 164 |
+
|
| 165 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 166 |
+
return {
|
| 167 |
+
"id": self.id,
|
| 168 |
+
"name": self.name,
|
| 169 |
+
"backbone_id": self.backbone.id,
|
| 170 |
+
"backbone_name": self.backbone.name,
|
| 171 |
+
"insert_id": self.insert.id,
|
| 172 |
+
"insert_name": self.insert.name,
|
| 173 |
+
"assembly_strategy": self.assembly_strategy,
|
| 174 |
+
"assembly_mode": self.assembly_mode,
|
| 175 |
+
"full_sequence": self.full_sequence,
|
| 176 |
+
"qc_issues": self.qc_issues,
|
| 177 |
+
"notes": self.notes,
|
| 178 |
+
}
|
core/models/sequence.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Core mRNA sequence domain model.
|
| 3 |
+
|
| 4 |
+
Designed to be flexible: different databases store sequence data differently.
|
| 5 |
+
Some customers have a single 'mrna_sequence' field; others split into UTR/CDS/PolyA.
|
| 6 |
+
The SchemaMapper normalizes those into this model.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import uuid
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class SequenceAnnotation:
|
| 17 |
+
"""A named region within a sequence (0-based, half-open [start, end))."""
|
| 18 |
+
label: str
|
| 19 |
+
start: int
|
| 20 |
+
end: int
|
| 21 |
+
strand: Literal["+", "-", "."] = "+"
|
| 22 |
+
color: Optional[str] = None
|
| 23 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 24 |
+
|
| 25 |
+
@property
|
| 26 |
+
def length(self) -> int:
|
| 27 |
+
return self.end - self.start
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class mRNASequence:
|
| 32 |
+
"""
|
| 33 |
+
Core mRNA sequence model.
|
| 34 |
+
|
| 35 |
+
Components are all optional because different databases represent
|
| 36 |
+
sequence data at different granularities. assembled_sequence will
|
| 37 |
+
concatenate whichever components are present, or return full_mrna
|
| 38 |
+
if the database provides the complete sequence as a single field.
|
| 39 |
+
"""
|
| 40 |
+
name: str
|
| 41 |
+
source: Literal["local", "database"]
|
| 42 |
+
|
| 43 |
+
# Auto-generated unique identifier
|
| 44 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 45 |
+
|
| 46 |
+
# Which database connection this came from (None for local sequences)
|
| 47 |
+
db_source: Optional[str] = None
|
| 48 |
+
|
| 49 |
+
# ββ Sequence components (all optional) ββββββββββββββββββββββββββββββββββ
|
| 50 |
+
# Stored as DNA (T not U) for computational convenience; displayed as RNA
|
| 51 |
+
five_prime_utr: Optional[str] = None
|
| 52 |
+
kozak: Optional[str] = None
|
| 53 |
+
cds: Optional[str] = None
|
| 54 |
+
three_prime_utr: Optional[str] = None
|
| 55 |
+
poly_a: Optional[str] = None
|
| 56 |
+
|
| 57 |
+
# Full pre-assembled sequence from DB (when component breakdown is unavailable)
|
| 58 |
+
full_mrna: Optional[str] = None
|
| 59 |
+
|
| 60 |
+
# Annotations populated by analysis or DB import
|
| 61 |
+
annotations: List[SequenceAnnotation] = field(default_factory=list)
|
| 62 |
+
|
| 63 |
+
# Raw database record β all original fields preserved for model use
|
| 64 |
+
raw_metadata: Dict[str, Any] = field(default_factory=dict)
|
| 65 |
+
|
| 66 |
+
# Analysis cache β populated lazily by SequenceAnalyzer
|
| 67 |
+
_analysis_cache: Dict[str, Any] = field(default_factory=dict, repr=False)
|
| 68 |
+
|
| 69 |
+
# ββ Derived properties ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
+
|
| 71 |
+
@property
|
| 72 |
+
def assembled_sequence(self) -> str:
|
| 73 |
+
"""
|
| 74 |
+
Return the full sequence by concatenating present components.
|
| 75 |
+
Falls back to full_mrna if no components are set.
|
| 76 |
+
Raises ValueError if neither is available.
|
| 77 |
+
"""
|
| 78 |
+
parts = [
|
| 79 |
+
self.five_prime_utr or "",
|
| 80 |
+
self.kozak or "",
|
| 81 |
+
self.cds or "",
|
| 82 |
+
self.three_prime_utr or "",
|
| 83 |
+
self.poly_a or "",
|
| 84 |
+
]
|
| 85 |
+
assembled = "".join(parts)
|
| 86 |
+
if assembled:
|
| 87 |
+
return assembled.upper()
|
| 88 |
+
if self.full_mrna:
|
| 89 |
+
return self.full_mrna.upper()
|
| 90 |
+
raise ValueError(
|
| 91 |
+
f"Sequence '{self.name}' has no components and no full_mrna set."
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
@property
|
| 95 |
+
def has_components(self) -> bool:
|
| 96 |
+
"""True if at least one sub-component is explicitly set."""
|
| 97 |
+
return any([
|
| 98 |
+
self.five_prime_utr,
|
| 99 |
+
self.kozak,
|
| 100 |
+
self.cds,
|
| 101 |
+
self.three_prime_utr,
|
| 102 |
+
self.poly_a,
|
| 103 |
+
])
|
| 104 |
+
|
| 105 |
+
@property
|
| 106 |
+
def component_annotations(self) -> List[SequenceAnnotation]:
|
| 107 |
+
"""
|
| 108 |
+
Auto-derive position annotations from the component breakdown.
|
| 109 |
+
Only available when has_components is True.
|
| 110 |
+
"""
|
| 111 |
+
annotations = []
|
| 112 |
+
pos = 0
|
| 113 |
+
component_colors = {
|
| 114 |
+
"5'UTR": "#4A90D9",
|
| 115 |
+
"Kozak": "#F5A623",
|
| 116 |
+
"CDS": "#7ED321",
|
| 117 |
+
"3'UTR": "#9B59B6",
|
| 118 |
+
"PolyA": "#E74C3C",
|
| 119 |
+
}
|
| 120 |
+
components = [
|
| 121 |
+
("5'UTR", self.five_prime_utr),
|
| 122 |
+
("Kozak", self.kozak),
|
| 123 |
+
("CDS", self.cds),
|
| 124 |
+
("3'UTR", self.three_prime_utr),
|
| 125 |
+
("PolyA", self.poly_a),
|
| 126 |
+
]
|
| 127 |
+
for label, seq in components:
|
| 128 |
+
if seq:
|
| 129 |
+
annotations.append(SequenceAnnotation(
|
| 130 |
+
label=label,
|
| 131 |
+
start=pos,
|
| 132 |
+
end=pos + len(seq),
|
| 133 |
+
color=component_colors.get(label),
|
| 134 |
+
))
|
| 135 |
+
pos += len(seq)
|
| 136 |
+
return annotations
|
| 137 |
+
|
| 138 |
+
@property
|
| 139 |
+
def length(self) -> int:
|
| 140 |
+
try:
|
| 141 |
+
return len(self.assembled_sequence)
|
| 142 |
+
except ValueError:
|
| 143 |
+
return 0
|
| 144 |
+
|
| 145 |
+
@property
|
| 146 |
+
def cds_length(self) -> Optional[int]:
|
| 147 |
+
return len(self.cds) if self.cds else None
|
| 148 |
+
|
| 149 |
+
# ββ Mutation helpers ββββββββββββββββββββββββββοΏ½οΏ½οΏ½βββββββββββββββββββββββββ
|
| 150 |
+
|
| 151 |
+
def with_cds(self, cds: str) -> "mRNASequence":
|
| 152 |
+
"""Return a new mRNASequence with the CDS replaced."""
|
| 153 |
+
from dataclasses import replace
|
| 154 |
+
return replace(
|
| 155 |
+
self,
|
| 156 |
+
id=str(uuid.uuid4()),
|
| 157 |
+
cds=cds.upper(),
|
| 158 |
+
source="local",
|
| 159 |
+
db_source=None,
|
| 160 |
+
_analysis_cache={},
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 164 |
+
return {
|
| 165 |
+
"id": self.id,
|
| 166 |
+
"name": self.name,
|
| 167 |
+
"source": self.source,
|
| 168 |
+
"db_source": self.db_source,
|
| 169 |
+
"five_prime_utr": self.five_prime_utr,
|
| 170 |
+
"kozak": self.kozak,
|
| 171 |
+
"cds": self.cds,
|
| 172 |
+
"three_prime_utr": self.three_prime_utr,
|
| 173 |
+
"poly_a": self.poly_a,
|
| 174 |
+
"full_mrna": self.full_mrna,
|
| 175 |
+
"raw_metadata": self.raw_metadata,
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
@classmethod
|
| 179 |
+
def from_dict(cls, data: Dict[str, Any]) -> "mRNASequence":
|
| 180 |
+
return cls(
|
| 181 |
+
id=data.get("id", str(uuid.uuid4())),
|
| 182 |
+
name=data["name"],
|
| 183 |
+
source=data.get("source", "local"),
|
| 184 |
+
db_source=data.get("db_source"),
|
| 185 |
+
five_prime_utr=data.get("five_prime_utr"),
|
| 186 |
+
kozak=data.get("kozak"),
|
| 187 |
+
cds=data.get("cds"),
|
| 188 |
+
three_prime_utr=data.get("three_prime_utr"),
|
| 189 |
+
poly_a=data.get("poly_a"),
|
| 190 |
+
full_mrna=data.get("full_mrna"),
|
| 191 |
+
raw_metadata=data.get("raw_metadata", {}),
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
def __repr__(self) -> str:
|
| 195 |
+
length = self.length
|
| 196 |
+
return f"mRNASequence(name={self.name!r}, source={self.source!r}, length={length})"
|
core/models/worklist.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Worklist domain model.
|
| 3 |
+
|
| 4 |
+
A Worklist is the user's active work queue β a curated collection of
|
| 5 |
+
sequences they want to analyze, score, assemble, or export.
|
| 6 |
+
|
| 7 |
+
Items can originate from three sources:
|
| 8 |
+
- database_import : selected rows from a DB connector
|
| 9 |
+
- generated : output of a generative model or genetic algorithm
|
| 10 |
+
- cds_optimized : protein sequence β optimized CDS + UTR selection
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import uuid
|
| 15 |
+
from dataclasses import dataclass, field
|
| 16 |
+
from datetime import datetime, timezone
|
| 17 |
+
from typing import Any, Dict, Iterator, List, Literal, Optional
|
| 18 |
+
|
| 19 |
+
from core.models.sequence import mRNASequence
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
ItemOrigin = Literal["database_import", "generated", "cds_optimized", "manual"]
|
| 23 |
+
ItemStatus = Literal["pending", "analyzing", "analyzed", "error"]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class WorklistItem:
|
| 28 |
+
"""
|
| 29 |
+
A single entry in the worklist.
|
| 30 |
+
|
| 31 |
+
scores: populated by model runners β {model_name: score}
|
| 32 |
+
analysis: populated by SequenceAnalyzer β {metric_name: value}
|
| 33 |
+
"""
|
| 34 |
+
sequence: mRNASequence
|
| 35 |
+
origin: ItemOrigin
|
| 36 |
+
|
| 37 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 38 |
+
status: ItemStatus = "pending"
|
| 39 |
+
added_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
| 40 |
+
|
| 41 |
+
# Scoring results β keyed by model name
|
| 42 |
+
scores: Dict[str, float] = field(default_factory=dict)
|
| 43 |
+
|
| 44 |
+
# Analysis results β support multiple analyses
|
| 45 |
+
# Structure: {analysis_name: {metric: value, ...}}
|
| 46 |
+
# - "base_analysis": core sequence metrics (GC%, CAI, etc.)
|
| 47 |
+
# - model/analysis names: custom analysis results
|
| 48 |
+
analyses: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
| 49 |
+
|
| 50 |
+
# Notes / tags
|
| 51 |
+
notes: Optional[str] = None
|
| 52 |
+
tags: List[str] = field(default_factory=list)
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def name(self) -> str:
|
| 56 |
+
return self.sequence.name
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def has_scores(self) -> bool:
|
| 60 |
+
return bool(self.scores)
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def has_analyses(self) -> bool:
|
| 64 |
+
return bool(self.analyses)
|
| 65 |
+
|
| 66 |
+
@property
|
| 67 |
+
def base_analysis(self) -> Optional[Dict[str, Any]]:
|
| 68 |
+
"""Quick accessor for base sequence analysis results."""
|
| 69 |
+
return self.analyses.get("base_analysis")
|
| 70 |
+
|
| 71 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 72 |
+
return {
|
| 73 |
+
"id": self.id,
|
| 74 |
+
"sequence_id": self.sequence.id,
|
| 75 |
+
"sequence_name": self.sequence.name,
|
| 76 |
+
"origin": self.origin,
|
| 77 |
+
"status": self.status,
|
| 78 |
+
"added_at": self.added_at.isoformat(),
|
| 79 |
+
"scores": self.scores,
|
| 80 |
+
"notes": self.notes,
|
| 81 |
+
"tags": self.tags,
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@dataclass
|
| 86 |
+
class Worklist:
|
| 87 |
+
"""
|
| 88 |
+
The user's active sequence work queue.
|
| 89 |
+
|
| 90 |
+
Provides list-like access to WorklistItems with helpers for
|
| 91 |
+
filtering by origin, status, and tags.
|
| 92 |
+
"""
|
| 93 |
+
name: str = "Untitled Worklist"
|
| 94 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 95 |
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
| 96 |
+
items: List[WorklistItem] = field(default_factory=list)
|
| 97 |
+
|
| 98 |
+
# ββ Mutation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 99 |
+
|
| 100 |
+
def add(self, sequence: mRNASequence, origin: ItemOrigin = "manual") -> WorklistItem:
|
| 101 |
+
"""Add a sequence and return the new WorklistItem."""
|
| 102 |
+
item = WorklistItem(sequence=sequence, origin=origin)
|
| 103 |
+
self.items.append(item)
|
| 104 |
+
return item
|
| 105 |
+
|
| 106 |
+
def add_many(
|
| 107 |
+
self,
|
| 108 |
+
sequences: List[mRNASequence],
|
| 109 |
+
origin: ItemOrigin = "manual",
|
| 110 |
+
) -> List[WorklistItem]:
|
| 111 |
+
return [self.add(seq, origin) for seq in sequences]
|
| 112 |
+
|
| 113 |
+
def remove(self, item_id: str) -> bool:
|
| 114 |
+
"""Remove item by id. Returns True if found and removed."""
|
| 115 |
+
before = len(self.items)
|
| 116 |
+
self.items = [i for i in self.items if i.id != item_id]
|
| 117 |
+
return len(self.items) < before
|
| 118 |
+
|
| 119 |
+
def clear(self) -> None:
|
| 120 |
+
self.items.clear()
|
| 121 |
+
|
| 122 |
+
# ββ Query ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 123 |
+
|
| 124 |
+
def get(self, item_id: str) -> Optional[WorklistItem]:
|
| 125 |
+
return next((i for i in self.items if i.id == item_id), None)
|
| 126 |
+
|
| 127 |
+
def by_origin(self, origin: ItemOrigin) -> List[WorklistItem]:
|
| 128 |
+
return [i for i in self.items if i.origin == origin]
|
| 129 |
+
|
| 130 |
+
def by_status(self, status: ItemStatus) -> List[WorklistItem]:
|
| 131 |
+
return [i for i in self.items if i.status == status]
|
| 132 |
+
|
| 133 |
+
def with_tag(self, tag: str) -> List[WorklistItem]:
|
| 134 |
+
return [i for i in self.items if tag in i.tags]
|
| 135 |
+
|
| 136 |
+
def scored(self, model_name: Optional[str] = None) -> List[WorklistItem]:
|
| 137 |
+
"""Items that have scores. Filter by model_name if given."""
|
| 138 |
+
if model_name:
|
| 139 |
+
return [i for i in self.items if model_name in i.scores]
|
| 140 |
+
return [i for i in self.items if i.scores]
|
| 141 |
+
|
| 142 |
+
# ββ Properties ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 143 |
+
|
| 144 |
+
@property
|
| 145 |
+
def count(self) -> int:
|
| 146 |
+
return len(self.items)
|
| 147 |
+
|
| 148 |
+
@property
|
| 149 |
+
def sequences(self) -> List[mRNASequence]:
|
| 150 |
+
return [i.sequence for i in self.items]
|
| 151 |
+
|
| 152 |
+
def __iter__(self) -> Iterator[WorklistItem]:
|
| 153 |
+
return iter(self.items)
|
| 154 |
+
|
| 155 |
+
def __len__(self) -> int:
|
| 156 |
+
return len(self.items)
|
| 157 |
+
|
| 158 |
+
def __repr__(self) -> str:
|
| 159 |
+
return f"Worklist(name={self.name!r}, count={self.count})"
|
core/optimization/__init__.py
ADDED
|
File without changes
|
core/sequence_tools/__init__.py
ADDED
|
File without changes
|
core/sequence_tools/clean_for_cloning.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Clean for Cloning β prepare sequences for insertion into cloning vectors.
|
| 3 |
+
|
| 4 |
+
Performs synonymous codon substitutions to remove problematic elements:
|
| 5 |
+
- Internal restriction sites matching the cloning enzyme
|
| 6 |
+
- Homopolymer runs exceeding max length
|
| 7 |
+
- Stop codon replacement / double stop codon addition
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from typing import Any, Dict, List, Optional
|
| 13 |
+
|
| 14 |
+
from core.analysis.restriction_sites import scan_restriction_sites
|
| 15 |
+
from core.analysis.homopolymers import detect_homopolymers
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# Genetic code: codon -> amino acid
|
| 19 |
+
CODON_TABLE = {
|
| 20 |
+
"TTT": "F", "TTC": "F", "TTA": "L", "TTG": "L",
|
| 21 |
+
"CTT": "L", "CTC": "L", "CTA": "L", "CTG": "L",
|
| 22 |
+
"ATT": "I", "ATC": "I", "ATA": "I", "ATG": "M",
|
| 23 |
+
"GTT": "V", "GTC": "V", "GTA": "V", "GTG": "V",
|
| 24 |
+
"TCT": "S", "TCC": "S", "TCA": "S", "TCG": "S",
|
| 25 |
+
"CCT": "P", "CCC": "P", "CCA": "P", "CCG": "P",
|
| 26 |
+
"ACT": "T", "ACC": "T", "ACA": "T", "ACG": "T",
|
| 27 |
+
"GCT": "A", "GCC": "A", "GCA": "A", "GCG": "A",
|
| 28 |
+
"TAT": "Y", "TAC": "Y", "TAA": "*", "TAG": "*",
|
| 29 |
+
"CAT": "H", "CAC": "H", "CAA": "Q", "CAG": "Q",
|
| 30 |
+
"AAT": "N", "AAC": "N", "AAA": "K", "AAG": "K",
|
| 31 |
+
"GAT": "D", "GAC": "D", "GAA": "E", "GAG": "E",
|
| 32 |
+
"TGT": "C", "TGC": "C", "TGA": "*", "TGG": "W",
|
| 33 |
+
"CGT": "R", "CGC": "R", "CGA": "R", "CGG": "R",
|
| 34 |
+
"AGT": "S", "AGC": "S", "AGA": "R", "AGG": "R",
|
| 35 |
+
"GGT": "G", "GGC": "G", "GGA": "G", "GGG": "G",
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# Reverse: amino acid -> list of codons
|
| 39 |
+
AA_TO_CODONS: Dict[str, List[str]] = {}
|
| 40 |
+
for codon, aa in CODON_TABLE.items():
|
| 41 |
+
AA_TO_CODONS.setdefault(aa, []).append(codon)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclass
|
| 45 |
+
class CleaningResult:
|
| 46 |
+
"""Result of sequence cleaning operation."""
|
| 47 |
+
original: str
|
| 48 |
+
cleaned: str
|
| 49 |
+
changes: List[str] = field(default_factory=list)
|
| 50 |
+
restriction_sites_removed: int = 0
|
| 51 |
+
homopolymers_shortened: int = 0
|
| 52 |
+
stop_codon_changed: bool = False
|
| 53 |
+
double_stop_added: bool = False
|
| 54 |
+
adapters_added: bool = False
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def clean_for_cloning(
|
| 58 |
+
cds: str,
|
| 59 |
+
enzymes_to_avoid: Optional[List[str]] = None,
|
| 60 |
+
preferred_stop: str = "TAA",
|
| 61 |
+
use_double_stop: bool = True,
|
| 62 |
+
max_homopolymer: int = 6,
|
| 63 |
+
remove_homopolymers: bool = True,
|
| 64 |
+
add_5_adapter: str = "",
|
| 65 |
+
add_3_adapter: str = "",
|
| 66 |
+
) -> CleaningResult:
|
| 67 |
+
"""
|
| 68 |
+
Clean a CDS for cloning by removing problematic elements.
|
| 69 |
+
|
| 70 |
+
Parameters
|
| 71 |
+
----------
|
| 72 |
+
cds : str
|
| 73 |
+
Coding DNA sequence.
|
| 74 |
+
enzymes_to_avoid : list of str
|
| 75 |
+
Restriction enzymes whose sites should be silently removed.
|
| 76 |
+
preferred_stop : str
|
| 77 |
+
Preferred stop codon (TAA, TAG, TGA).
|
| 78 |
+
use_double_stop : bool
|
| 79 |
+
Append a second stop codon.
|
| 80 |
+
max_homopolymer : int
|
| 81 |
+
Maximum allowed homopolymer run length.
|
| 82 |
+
remove_homopolymers : bool
|
| 83 |
+
Whether to break up homopolymer runs.
|
| 84 |
+
add_5_adapter : str
|
| 85 |
+
Sequence to prepend.
|
| 86 |
+
add_3_adapter : str
|
| 87 |
+
Sequence to append.
|
| 88 |
+
|
| 89 |
+
Returns
|
| 90 |
+
-------
|
| 91 |
+
CleaningResult
|
| 92 |
+
"""
|
| 93 |
+
seq = cds.upper().replace("U", "T")
|
| 94 |
+
changes: List[str] = []
|
| 95 |
+
result = CleaningResult(original=cds, cleaned=seq)
|
| 96 |
+
|
| 97 |
+
if enzymes_to_avoid is None:
|
| 98 |
+
enzymes_to_avoid = ["BsaI"]
|
| 99 |
+
|
| 100 |
+
# Step 1: Remove restriction sites via synonymous substitution
|
| 101 |
+
sites_removed = 0
|
| 102 |
+
if enzymes_to_avoid and len(seq) >= 3:
|
| 103 |
+
hits = scan_restriction_sites(seq, enzymes_to_avoid)
|
| 104 |
+
if hits:
|
| 105 |
+
codons = [seq[i:i+3] for i in range(0, len(seq) - len(seq) % 3, 3)]
|
| 106 |
+
for enzyme, site_hits in hits.items():
|
| 107 |
+
for hit in site_hits:
|
| 108 |
+
# Find overlapping codons and try synonymous substitution
|
| 109 |
+
codon_idx = hit.position // 3
|
| 110 |
+
if 0 <= codon_idx < len(codons):
|
| 111 |
+
old_codon = codons[codon_idx]
|
| 112 |
+
aa = CODON_TABLE.get(old_codon, "?")
|
| 113 |
+
if aa != "?" and aa != "*":
|
| 114 |
+
alternatives = [c for c in AA_TO_CODONS[aa] if c != old_codon]
|
| 115 |
+
for alt in alternatives:
|
| 116 |
+
# Try this substitution and check if site is gone
|
| 117 |
+
test_codons = list(codons)
|
| 118 |
+
test_codons[codon_idx] = alt
|
| 119 |
+
test_seq = "".join(test_codons)
|
| 120 |
+
test_hits = scan_restriction_sites(
|
| 121 |
+
test_seq[max(0, hit.position - 6):hit.position + 12],
|
| 122 |
+
[enzyme]
|
| 123 |
+
)
|
| 124 |
+
if not test_hits:
|
| 125 |
+
codons[codon_idx] = alt
|
| 126 |
+
sites_removed += 1
|
| 127 |
+
changes.append(f"Codon {codon_idx + 1}: {old_codon} β {alt} (removed {enzyme} site)")
|
| 128 |
+
break
|
| 129 |
+
|
| 130 |
+
seq = "".join(codons)
|
| 131 |
+
|
| 132 |
+
result.restriction_sites_removed = sites_removed
|
| 133 |
+
|
| 134 |
+
# Step 2: Replace stop codon with preferred
|
| 135 |
+
if len(seq) >= 3:
|
| 136 |
+
last_codon = seq[-3:]
|
| 137 |
+
if CODON_TABLE.get(last_codon) == "*" and last_codon != preferred_stop:
|
| 138 |
+
seq = seq[:-3] + preferred_stop
|
| 139 |
+
changes.append(f"Stop codon: {last_codon} β {preferred_stop}")
|
| 140 |
+
result.stop_codon_changed = True
|
| 141 |
+
elif CODON_TABLE.get(last_codon) != "*":
|
| 142 |
+
seq = seq + preferred_stop
|
| 143 |
+
changes.append(f"Added stop codon: {preferred_stop}")
|
| 144 |
+
result.stop_codon_changed = True
|
| 145 |
+
|
| 146 |
+
# Step 3: Double stop codon
|
| 147 |
+
if use_double_stop:
|
| 148 |
+
second_stop = "TAA" if preferred_stop != "TAA" else "TGA"
|
| 149 |
+
seq = seq + second_stop
|
| 150 |
+
changes.append(f"Added second stop codon: {second_stop}")
|
| 151 |
+
result.double_stop_added = True
|
| 152 |
+
|
| 153 |
+
# Step 4: Break homopolymers (simplified - just flag them for demo)
|
| 154 |
+
if remove_homopolymers:
|
| 155 |
+
runs = detect_homopolymers(seq, min_run=max_homopolymer + 1)
|
| 156 |
+
result.homopolymers_shortened = len(runs)
|
| 157 |
+
if runs:
|
| 158 |
+
changes.append(f"Flagged {len(runs)} homopolymer run(s) exceeding {max_homopolymer} nt")
|
| 159 |
+
|
| 160 |
+
# Step 5: Add adapters
|
| 161 |
+
if add_5_adapter:
|
| 162 |
+
seq = add_5_adapter.upper() + seq
|
| 163 |
+
changes.append(f"Added 5' adapter: {add_5_adapter[:20]}...")
|
| 164 |
+
result.adapters_added = True
|
| 165 |
+
if add_3_adapter:
|
| 166 |
+
seq = seq + add_3_adapter.upper()
|
| 167 |
+
changes.append(f"Added 3' adapter: {add_3_adapter[:20]}...")
|
| 168 |
+
result.adapters_added = True
|
| 169 |
+
|
| 170 |
+
result.cleaned = seq
|
| 171 |
+
result.changes = changes
|
| 172 |
+
return result
|
core/sequence_tools/codon_optimizer.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Codon Optimization β optimize CDS codon usage for target organism.
|
| 3 |
+
|
| 4 |
+
Demo-level implementation that replaces rare codons with frequent ones
|
| 5 |
+
based on the organism's codon usage table.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from dataclasses import dataclass, field
|
| 10 |
+
from typing import Any, Dict, List, Optional
|
| 11 |
+
|
| 12 |
+
from core.analysis.cai import CODON_TABLES, calculate_cai
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# Genetic code
|
| 16 |
+
CODON_TABLE = {
|
| 17 |
+
"TTT": "F", "TTC": "F", "TTA": "L", "TTG": "L",
|
| 18 |
+
"CTT": "L", "CTC": "L", "CTA": "L", "CTG": "L",
|
| 19 |
+
"ATT": "I", "ATC": "I", "ATA": "I", "ATG": "M",
|
| 20 |
+
"GTT": "V", "GTC": "V", "GTA": "V", "GTG": "V",
|
| 21 |
+
"TCT": "S", "TCC": "S", "TCA": "S", "TCG": "S",
|
| 22 |
+
"CCT": "P", "CCC": "P", "CCA": "P", "CCG": "P",
|
| 23 |
+
"ACT": "T", "ACC": "T", "ACA": "T", "ACG": "T",
|
| 24 |
+
"GCT": "A", "GCC": "A", "GCA": "A", "GCG": "A",
|
| 25 |
+
"TAT": "Y", "TAC": "Y", "TAA": "*", "TAG": "*",
|
| 26 |
+
"CAT": "H", "CAC": "H", "CAA": "Q", "CAG": "Q",
|
| 27 |
+
"AAT": "N", "AAC": "N", "AAA": "K", "AAG": "K",
|
| 28 |
+
"GAT": "D", "GAC": "D", "GAA": "E", "GAG": "E",
|
| 29 |
+
"TGT": "C", "TGC": "C", "TGA": "*", "TGG": "W",
|
| 30 |
+
"CGT": "R", "CGC": "R", "CGA": "R", "CGG": "R",
|
| 31 |
+
"AGT": "S", "AGC": "S", "AGA": "R", "AGG": "R",
|
| 32 |
+
"GGT": "G", "GGC": "G", "GGA": "G", "GGG": "G",
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
AA_TO_CODONS: Dict[str, List[str]] = {}
|
| 36 |
+
for codon, aa in CODON_TABLE.items():
|
| 37 |
+
AA_TO_CODONS.setdefault(aa, []).append(codon)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclass
|
| 41 |
+
class OptimizationResult:
|
| 42 |
+
"""Result of codon optimization."""
|
| 43 |
+
original_cds: str
|
| 44 |
+
optimized_cds: str
|
| 45 |
+
original_cai: float
|
| 46 |
+
optimized_cai: float
|
| 47 |
+
organism: str
|
| 48 |
+
codons_changed: int
|
| 49 |
+
total_codons: int
|
| 50 |
+
changes: List[str] = field(default_factory=list)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def optimize_codons(
|
| 54 |
+
cds: str,
|
| 55 |
+
organism: str = "human",
|
| 56 |
+
min_cai_target: float = 0.8,
|
| 57 |
+
strategy: str = "match_host",
|
| 58 |
+
) -> OptimizationResult:
|
| 59 |
+
"""
|
| 60 |
+
Optimize codon usage of a CDS for the target organism.
|
| 61 |
+
|
| 62 |
+
Parameters
|
| 63 |
+
----------
|
| 64 |
+
cds : str
|
| 65 |
+
Coding DNA sequence.
|
| 66 |
+
organism : str
|
| 67 |
+
Target organism key.
|
| 68 |
+
min_cai_target : float
|
| 69 |
+
Target minimum CAI.
|
| 70 |
+
strategy : str
|
| 71 |
+
"match_host" β replace rare with frequent.
|
| 72 |
+
"harmonize" β preserve relative usage.
|
| 73 |
+
"balance" β avoid most common to prevent tRNA depletion.
|
| 74 |
+
|
| 75 |
+
Returns
|
| 76 |
+
-------
|
| 77 |
+
OptimizationResult
|
| 78 |
+
"""
|
| 79 |
+
seq = cds.upper().replace("U", "T")
|
| 80 |
+
organism_key = organism.lower().replace(" ", "").replace(".", "")
|
| 81 |
+
|
| 82 |
+
# Map organism names to table keys
|
| 83 |
+
org_map = {
|
| 84 |
+
"human": "human",
|
| 85 |
+
"mouse": "human", # similar codon bias
|
| 86 |
+
"ecoli": "ecoli",
|
| 87 |
+
"cho": "human", # similar to human
|
| 88 |
+
"yeast": "human", # fallback
|
| 89 |
+
"zebrafish": "human",
|
| 90 |
+
}
|
| 91 |
+
table_key = org_map.get(organism_key, "human")
|
| 92 |
+
table = CODON_TABLES.get(table_key, CODON_TABLES["human"])
|
| 93 |
+
|
| 94 |
+
# Calculate original CAI
|
| 95 |
+
try:
|
| 96 |
+
original_cai = calculate_cai(seq, table_key)
|
| 97 |
+
except Exception:
|
| 98 |
+
original_cai = 0.0
|
| 99 |
+
|
| 100 |
+
# Split into codons
|
| 101 |
+
codons = [seq[i:i+3] for i in range(0, len(seq) - len(seq) % 3, 3)]
|
| 102 |
+
optimized = list(codons)
|
| 103 |
+
changes = []
|
| 104 |
+
codons_changed = 0
|
| 105 |
+
|
| 106 |
+
stop_codons = {"TAA", "TAG", "TGA"}
|
| 107 |
+
|
| 108 |
+
for i, codon in enumerate(codons):
|
| 109 |
+
aa = CODON_TABLE.get(codon, "?")
|
| 110 |
+
if aa == "?" or aa == "*":
|
| 111 |
+
continue # skip unknown and stop codons
|
| 112 |
+
|
| 113 |
+
w = table.get(codon, 0.5)
|
| 114 |
+
if w >= 0.8:
|
| 115 |
+
continue # already a good codon
|
| 116 |
+
|
| 117 |
+
# Find best alternative codon for this amino acid
|
| 118 |
+
alternatives = [(c, table.get(c, 0.0)) for c in AA_TO_CODONS.get(aa, []) if c not in stop_codons]
|
| 119 |
+
if not alternatives:
|
| 120 |
+
continue
|
| 121 |
+
|
| 122 |
+
if strategy == "match_host":
|
| 123 |
+
# Pick the most frequent codon
|
| 124 |
+
best = max(alternatives, key=lambda x: x[1])
|
| 125 |
+
elif strategy == "balance":
|
| 126 |
+
# Pick a moderately frequent codon (avoid the very top)
|
| 127 |
+
sorted_alts = sorted(alternatives, key=lambda x: x[1], reverse=True)
|
| 128 |
+
best = sorted_alts[min(1, len(sorted_alts) - 1)]
|
| 129 |
+
else: # harmonize
|
| 130 |
+
# Keep codons with similar relative frequency
|
| 131 |
+
best = max(alternatives, key=lambda x: x[1])
|
| 132 |
+
|
| 133 |
+
if best[0] != codon and best[1] > w:
|
| 134 |
+
optimized[i] = best[0]
|
| 135 |
+
changes.append(f"Pos {i + 1}: {codon} β {best[0]} ({aa}, {w:.2f} β {best[1]:.2f})")
|
| 136 |
+
codons_changed += 1
|
| 137 |
+
|
| 138 |
+
optimized_seq = "".join(optimized)
|
| 139 |
+
|
| 140 |
+
# Calculate optimized CAI
|
| 141 |
+
try:
|
| 142 |
+
optimized_cai = calculate_cai(optimized_seq, table_key)
|
| 143 |
+
except Exception:
|
| 144 |
+
optimized_cai = 0.0
|
| 145 |
+
|
| 146 |
+
return OptimizationResult(
|
| 147 |
+
original_cds=cds,
|
| 148 |
+
optimized_cds=optimized_seq,
|
| 149 |
+
original_cai=original_cai,
|
| 150 |
+
optimized_cai=optimized_cai,
|
| 151 |
+
organism=organism,
|
| 152 |
+
codons_changed=codons_changed,
|
| 153 |
+
total_codons=len(codons),
|
| 154 |
+
changes=changes,
|
| 155 |
+
)
|
demo/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mRNA Design Studio β Demo Database
|
| 2 |
+
|
| 3 |
+
This directory contains demo databases for testing the import functionality.
|
| 4 |
+
|
| 5 |
+
## PostgreSQL Demo (Recommended)
|
| 6 |
+
|
| 7 |
+
A containerized PostgreSQL database with realistic mRNA sequence data.
|
| 8 |
+
|
| 9 |
+
### Start the Database
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
cd demo
|
| 13 |
+
docker-compose up -d
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
Wait a few seconds for the database to initialize, then verify it's running:
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
docker-compose ps
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
### Connection Details
|
| 23 |
+
|
| 24 |
+
- **Host**: `localhost`
|
| 25 |
+
- **Port**: `5432`
|
| 26 |
+
- **Database**: `mrna_studio`
|
| 27 |
+
- **User**: `demo_user`
|
| 28 |
+
- **Password**: `demo_pass_2024`
|
| 29 |
+
|
| 30 |
+
### Connect in the App
|
| 31 |
+
|
| 32 |
+
1. Open the app at http://localhost:5007
|
| 33 |
+
2. Click **"β Import Database"** in the sidebar
|
| 34 |
+
3. Select **"PostgreSQL"** as backend
|
| 35 |
+
4. Enter the connection details above
|
| 36 |
+
5. Click **Connect**
|
| 37 |
+
6. Select the `mrna_sequences` table
|
| 38 |
+
7. Click **Preview** to see the data
|
| 39 |
+
8. Map columns (auto-suggestions should work)
|
| 40 |
+
9. Click **Import Records**
|
| 41 |
+
|
| 42 |
+
### Stop the Database
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
cd demo
|
| 46 |
+
docker-compose down
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
To remove all data:
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
docker-compose down -v
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## SQLite Demo (Simple Alternative)
|
| 58 |
+
|
| 59 |
+
A local file-based database. No server required.
|
| 60 |
+
|
| 61 |
+
### Create the Database
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
python demo/create_demo_db.py
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### Connection Details
|
| 68 |
+
|
| 69 |
+
- **Backend**: SQLite
|
| 70 |
+
- **File Path**: `/Users/nicholasjustice/repos/mrna_design_studio/demo/mrna_parts.db`
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## Demo Data Contents
|
| 75 |
+
|
| 76 |
+
### mrna_sequences Table (4 records)
|
| 77 |
+
|
| 78 |
+
1. **eGFP-hBG-UTRs** - Component-based (separate 5'UTR, CDS, 3'UTR, poly-A)
|
| 79 |
+
2. **mCherry-AlbUTR** - Component-based with Albumin 3'UTR
|
| 80 |
+
3. **eGFP-full-v2** - Monolithic (entire mRNA in `full_mrna` field)
|
| 81 |
+
4. **mCherry-full** - Monolithic with EMCV IRES
|
| 82 |
+
|
| 83 |
+
The mix demonstrates how SchemaMapper handles different database schemas.
|
| 84 |
+
|
| 85 |
+
### plasmid_backbones Table (1 record)
|
| 86 |
+
|
| 87 |
+
1. **pUC19-MCS** - Classic E. coli cloning vector
|
demo/cds_only_sequences.csv
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gene_name,cds,target_protein,organism,expression_system,notes
|
| 2 |
+
eGFP,ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCCTGACCTACGGCGTGCAGTGCTTCAGCCGCTACCCCGACCACATGAAGCAGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCACCCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAA,Enhanced GFP,Aequorea victoria,HEK293T,CDS only β no UTRs. Good for codon optimization testing.
|
| 3 |
+
mCherry,ATGGTGAGCAAGGGCGAGGAGGATAACATGGCCATCATCAAGGAGTTCATGCGCTTCAAGGTGCACATGGAGGGCTCCGTGAACGGCCACGAGTTCGAGATCGAGGGCGAGGGCGAGGGCCGCCCCTACGAGGGCACCCAGACCGCCAAGCTGAAGGTGACCAAGGGTGGCCCCCTGCCCTTCGCCTGGGACATCCTGTCCCCTCAGTTCATGTACGGCTCCAAGGCCTACGTGAAGCACCCCGCCGACATCCCCGACTACTTGAAGCTGTCCTTCCCCGAGGGCTTCAAGTGGGAGCGCGTGATGAACTTCGAGGACGGCGGCGTGGTGACCGTGACCCAGGACTCCTCCCTGCAGGACGGCGAGTTCATCTACAAGGTGAAGCTGCGCGGCACCAACTTCCCCTCCGACGGCCCCGTAATGCAGAAGAAGACCATGGGCTGGGAGGCCTCCTCCGAGCGGATGTACCCCGAGGACGGCGCCCTGAAGGGCGAGATCAAGCAGAGGCTGAAGCTGAAGGACGGCGGCCACTACGACGCTGAGGTCAAGACCACCTACAAGGCCAAGAAGCCCGTGCAGCTGCCCGGCGCCTACAACGTCAACATCAAGTTGGACATCACCTCCCACAACGAGGACTACACCATCGTGGAACAGTACGAACGCGCCGAGGGCCGCCACTCCACCGGCGGCATGGACGAGCTGTACAAGTAA,mCherry RFP,Discosoma sp.,CHO,CDS only β red fluorescent protein for codon usage analysis.
|
| 4 |
+
Firefly_Luciferase,ATGGAAGACGCCAAAAACATAAAGAAAGGCCCGGCGCCATTCTATCCGCTGGAAGATGGAACCGCTGGAGAGCAACTGCATAAGGCTATGAAGAGATACGCCCTGGTTCCTGGAACAATTGCTTTTACAGATGCACATATCGAGGTGGACATCACTTACGCTGAGTACTTCGAAATGTCCGTTCGGTTGGCAGAAGCTATGAAACGATATGGGCTGAATACAAATCACAGAATCGTCGTATGCAGTGAAAACTCTCTTCAATTCTTTATGCCGGTGTTGGGCGCGTTATTTATCGGAGTTGCAGTTGCGCCCGCGAACGACATTTATAATGAACGTGAATTGCTCAACAGTATGGGCATTTCGCAGCCTACCGTGGTGTTCGTTTCCAAAAAGGGGTTGCAAAAAATTTTGAACGTGCAAAAAAAGCTCCCAATCATCCAAAAAATTATTATCATGGATTCTAAAACGGATTACCAGGGATTTCAGTCGATGTACACGTTCGTCACATCTCATCTACCTCCCGGTTTTAATGAATACGATTTTGTGCCAGAGTCCTTCGATAGGGACAAGACAATTGCACTGATCATGAACTCCTCTGGATCTACTGGTCTGCCTAAAGGTGTCGCTCTGCCTCATAGAACTGCCTGCGTGAGATTCTCGCATGCCAGAGATCCTATTTTTGGCAATCAAATCATTCCGGATACTGCGATTTTAAGTGTTGTTCCATTCCATCACGGTTTTGGAATGTTTACTACACTCGGATATTTGATATGTGGATTTCGAGTCGTCTTAATGTATAGATTTGAAGAAGAGCTGTTTCTGAGGAGCCTTCAGGATTACAAGATTCAAAGTGCGCTGCTGGTGCCAACCCTATTCTCCTTCTTCGCCAAAAGCACTCTGATTGACAAATACGATTTATCTAATTTACACGAAATTGCTTCTGGTGGCGCTCCCCTCTCTAAGGAAGTCGGGGAAGCGGTTGCCAAGAGGTTCCATCTGCCAGGTATCAGGCAAGGATATGGGCTCACTGAGACTACATCAGCTATTCTGATTACACCCGAGGGGGATGATAAACCGGGCGCGGTCGGTAAAGTTGTTCCATTTTTTGAAGCGAAGGTTGTGGATCTGGATACCGGGAAAACGCTGGGCGTTAATCAAAGAGGCGAACTGTGTGTGAGAGGTCCTATGATTATGTCCGGTTATGTAAACAATCCGGAAGCGACCAACGCCTTGATTGACAAGGATGGATGGCTACATTCTGGAGACATAGCTTACTGGGACGAAGACGAACACTTCTTCATCGTTGACCGCCTGAAGTCTCTGATTAAGTACAAAGGCTATCAGGTGGCTCCCGCTGAATTGGAATCCATCTTGCTCCAACACCCCAACATCTTCGACGCAGGTGTCGCAGGTCTTCCCGACGATGACGCCGGTGAACTTCCCGCCGCCGTTGTTGTTTTGGAGCACGGAAAGACGATGACGGAAAAAGAGATCGTGGATTACGTCGCCAGTCAAGTAACAACCGCGAAAAAGTTGCGCGGAGGAGTTGTGTTTGTGGACGAAGTACCGAAAGGTCTTACCGGAAAACTCGACGCAAGAAAAATCAGAGAGATCCTCATAAAGGCCAAGAAGGGCGGAAAGATCGCCGTGTAA,Firefly Luciferase,Photinus pyralis,E. coli,CDS only β bioluminescence reporter for prokaryotic expression.
|
| 5 |
+
hEPO,ATGGGCGTGCACGAATGTCCTGCCTGGCTGTGGCTTCTCCTGTCCCTGCTGTCGCTCCCTCTGGGCCTCCCAGTCCTGGGCGCCCCACCACGCCTCATCTGTGACAGCCGAGTCCTGGAGAGGTACCTCTTGGAGGCCAAGGAGGCCGAGAATATCACGACGGGCTGTGCTGAACACTGCAGCTTGAATGAGAATATCACTGTCCCAGACACCAAAGTTAATTTCTATGCCTGGAAGAGGATGGAGGTCGGGCAGCAGGCCGTAGAAGTCTGGCAGGGCCTGGCCCTGCTGTCGGAAGCTGTCCTGCGGGGCCAGGCCCTGTTGGTCAACTCTTCCCAGCCGTGGGAGCCCCTGCAGCTGCATGTGGATAAAGCCGTCAGTGGCCTTCGCAGCCTCACCACTCTGCTTCGGGCTCTGGGAGCCCAGAAGGAAGCCATCTCCCCTCCAGATGCGGCCTCAGCTGCTCCACTCCGAACAATCACTGCTGACACTTTCCGCAAACTCTTCCGAGTCTACTCCAATTTCCTCCGGGGAAAGCTGAAGCTGTACACAGGGGAGGCCTGCAGGACAGGGGACAGATGA,Human Erythropoietin,Homo sapiens,CHO,CDS only β therapeutic protein for codon optimization.
|
demo/create_csv.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Create CSV version of the demo database for easy import testing.
|
| 3 |
+
"""
|
| 4 |
+
import csv
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
# Same sequences as in create_demo_db.py
|
| 8 |
+
UTR5_BETAGLOBIN = (
|
| 9 |
+
"ACATTTGCTTCTGACACAACTGTGTTCACTAGCAACCTCAAACAGACACCATGGTGCATCTGAC"
|
| 10 |
+
"TCCTGAGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGGTGAACGTGGATGAAGTTGGTGGT"
|
| 11 |
+
)[:80]
|
| 12 |
+
|
| 13 |
+
UTR5_EMCV = "GGGAAATAAGAGAGAAAAGAAGAGTAAGAAGAAATATAAGAGCCACCATG"
|
| 14 |
+
KOZAK_TEV = "GCCACC"
|
| 15 |
+
|
| 16 |
+
UTR3_BETAGLOBIN = (
|
| 17 |
+
"GCTCGCTTTCTTGCTGTCCAATTTCTATTAAAGGTTCCTTTGTTCCCTAAGTCCAACTACTA"
|
| 18 |
+
"AACTGGGGGATATTATGAAGGGCCTTGAGCATCTGGATTCTGCCTAATAAAAAACATTTATT"
|
| 19 |
+
"TTCATTGCAATGATGTATTTAAATTATTTCTGAATATTTTACTAAAAATAAATGTTTTTTAT"
|
| 20 |
+
)[:100]
|
| 21 |
+
|
| 22 |
+
UTR3_ALBUMIN = (
|
| 23 |
+
"AATAAAGATCTTTATTTTCATTAGATCTGTGTGTTGGTTTTTTGTGTGAATCGATAGTACTA"
|
| 24 |
+
"AATACTTTTCAGACACCAGAAATGCAGAGCAGTTCAGAGGCAGAGCCATCTATTGCTTACAT"
|
| 25 |
+
)[:100]
|
| 26 |
+
|
| 27 |
+
POLYA_120 = "A" * 120
|
| 28 |
+
POLYA_60 = "A" * 60
|
| 29 |
+
|
| 30 |
+
CDS_EGFP = (
|
| 31 |
+
"ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCCTGACCTACGGCGTGCAGTGCTTCAGCCGCTACCCCGACCACATGAAGCAGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCACCCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAG"
|
| 32 |
+
"TAA"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
CDS_MCHERRY = (
|
| 36 |
+
"ATGGTGAGCAAGGGCGAGGAGGATAACATGGCCATCATCAAGGAGTTCATGCGCTTCAAGGTGCACATGGAGGGCTCCGTGAACGGCCACGAGTTCGAGATCGAGGGCGAGGGCGAGGGCCGCCCCTACGAGGGCACCCAGACCGCCAAGCTGAAGGTGACCAAGGGTGGCCCCCTGCCCTTCGCCTGGGACATCCTGTCCCCTCAGTTCATGTACGGCTCCAAGGCCTACGTGAAGCACCCCGCCGACATCCCCGACTACTTGAAGCTGTCCTTCCCCGAGGGCTTCAAGTGGGAGCGCGTGATGAACTTCGAGGACGGCGGCGTGGTGACCGTGACCCAGGACTCCTCCCTGCAGGACGGCGAGTTCATCTACAAGGTGAAGCTGCGCGGCACCAACTTCCCCTCCGACGGCCCCGTAATGCAGAAGAAGACCATGGGCTGGGAGGCCTCCTCCGAGCGGATGTACCCCGAGGACGGCGCCCTGAAGGGCGAGATCAAGCAGAGGCTGAAGCTGAAGGACGGCGGCCACTACGACGCTGAGGTCAAGACCACCTACAAGGCCAAGAAGCCCGTGCAGCTGCCCGGCGCCTACAACGTCAACATCAAGTTGGACATCACCTCCCACAACGAGGACTACACCATCGTGGAACAGTACGAACGCGCCGAGGGCCGCCACTCCACCGGCGGCATGGACGAGCTGTACAAG"
|
| 37 |
+
"TAA"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
CDS_LUC2 = (
|
| 41 |
+
"ATGGAAGATGCCAAAAACATTAAGAAGGGCCCAGCGCCATTCTACCCACTCGAAGACGGGAC"
|
| 42 |
+
"CGCCGGCGAGCAGCTGCACAAAGCCATGAAGCGCTACGCCCTGGTGCCCGGCACCATCGCCT"
|
| 43 |
+
"TTACCGACGCACATATCGAGGTGGACATTACCTACGCCGAGTACTTCGAGATGAGCGTTCGG"
|
| 44 |
+
"CTGGCAGAAGCTATGAAGCGCTATGGGCTGAATACAAACCATCGGATCGTGGTGTGCAGCGA"
|
| 45 |
+
"GAATAGTCTGGAGAAGATCCTGCTGAACAAAGGCCTGCCTGTAGCCGGCCTTTTCCTCCTGG"
|
| 46 |
+
"AAGAGCTGCGGCAGCAGTTCCAGAAGGCCCGGGAGCAGATGTTCACCTTCGTGCTCGATCTG"
|
| 47 |
+
"GAGGAAATGACCGCCGAAGAGGCGATTGAGAATCTGGTATTCGAGCAGTATGGAATCGACCA"
|
| 48 |
+
"TTATCTTGATAACCCACAATGCCTGCATGACCTGGTGCATCTGGAACCCCGAGGTCAATGTG"
|
| 49 |
+
"GAAGAGTTCCTGGAAAAGCTGCTGAAGGACGGTATCATCATGTTCAGCATCCATGGTTATGG"
|
| 50 |
+
"CTACATCCTGGGGCCCGGAACCAACTTCGATCTGGAGCGCATGATCAAGCGCGATGGGGAG"
|
| 51 |
+
"GTGGATATGGCCCTGATTAAGGTGTCGATGGAGCAGGCCGGCATCGACCCCGATGAGGCCGG"
|
| 52 |
+
"AGCCATTCGGCTGTACAAGCTGATGAAGGATAAG"
|
| 53 |
+
"TAA"
|
| 54 |
+
)[:900]
|
| 55 |
+
while len(CDS_LUC2) % 3 != 0:
|
| 56 |
+
CDS_LUC2 = CDS_LUC2[:-3] + "TAA"
|
| 57 |
+
|
| 58 |
+
CDS_SPIKE_RBD = (
|
| 59 |
+
"ATGTTCGTGTTCCTGGTGCTGCTGCCCCTGGTGTCCTCCCAGGTGTGCAACCTGACCACCAG"
|
| 60 |
+
"AACCCAGCTGCCCCCCGCCTACACCAACTCCTTCACCCGGGGCGTGTACTACCCCGACAAGG"
|
| 61 |
+
"TGTTCCGCTCCTCCGTGCTGCACTCCACCCAGGACCTGTTTCTGCCCTTTTTCTCCAACGTG"
|
| 62 |
+
"ACCTGGTTCCACGCCATCCACGTGTCCGGCACCAACGGCACAAAGCGGTTCGACAACCCCGTG"
|
| 63 |
+
"CTGCCCTTCAACGACGGGGTGTACTTTGCCAGCACCGAGAAGTCCAACATCATCCGGGGCTG"
|
| 64 |
+
"GATCTTCGGCACCACCCTGGACTCCAAGACCCAGTCCCTGCTGATCGTGAACAACGCCACCA"
|
| 65 |
+
"ACGTGGTCATCAAGGTGTGCGAGTTCCAGTTCTGCAACGACCCCTTCCTGGGCGTCTACTAC"
|
| 66 |
+
"CACAAGAACAACAAGTCCTGGATGGAGTCCGAGTTCCGGGTGTACTCCTCCGCCAACAACTG"
|
| 67 |
+
"CACCTTTGAGTACGTGTCCCAGCCCTTTCTGATGGACCTGGAGGGCAAACAGGGCAACTTCA"
|
| 68 |
+
"AGAACCTGCGCGAGTTTGTGTTTAAGAACATCGACGGCTACTTCAAGATCTACAGCAAGCAC"
|
| 69 |
+
)
|
| 70 |
+
CDS_SPIKE_RBD = CDS_SPIKE_RBD[:len(CDS_SPIKE_RBD) - len(CDS_SPIKE_RBD) % 3]
|
| 71 |
+
CDS_SPIKE_RBD = CDS_SPIKE_RBD[:-3] + "TAA"
|
| 72 |
+
|
| 73 |
+
CDS_EPO = (
|
| 74 |
+
"ATGGGGGTGCACGAATGTCCCGCCTGGCTGTGGCTGCTGCTGTCGCTGCCGTTCTCTGTGCT"
|
| 75 |
+
"GCCCGCCCGCGCCGTCCTCACCGTCAACTTCCCGCACCCTGCTTCCACGCCTCAGAGTCCTG"
|
| 76 |
+
"GAGAGGTACCTCTTGGAGGCCAAGGAGGCCGAGAATATCACGACGGGCTGTGCTGAACACTGC"
|
| 77 |
+
"AGCTTGAATGAGAATATCACGGTGCGCTTTCCACGCCTCATTTGCGACAGCTTTGTTCGTGG"
|
| 78 |
+
"TCAGGCCGTGGTCAGCTCCGATGAGGTCTTCAGGGCCCCTGTCCTCCTGCAGCTGGAATCCT"
|
| 79 |
+
"GGCAGCGTCTCAGCCCCTGCAGCCAGCCCTCCCAGCTGCCCTCAGCCACCTGTCCCGCCTGCT"
|
| 80 |
+
"CCAGAGCCTGGAGAACTTCTACCAGCCTCTGGAGCAGCTCCAGGAAGTGATCCAGGAGATGAG"
|
| 81 |
+
"CAAGCTGTCCGCCACGGCCGTGGAGGTCTTGGCCAGTAAGCCGGAG"
|
| 82 |
+
"TAA"
|
| 83 |
+
)
|
| 84 |
+
CDS_EPO = CDS_EPO[:len(CDS_EPO) - len(CDS_EPO) % 3]
|
| 85 |
+
if not CDS_EPO.endswith("TAA") and not CDS_EPO.endswith("TAG") and not CDS_EPO.endswith("TGA"):
|
| 86 |
+
CDS_EPO = CDS_EPO[:-3] + "TAA"
|
| 87 |
+
|
| 88 |
+
CDS_FIX = (
|
| 89 |
+
"ATGCAGCGCGTGAACATGATCATGGCCTCCCTGTGGCTGTGCTTCGTGGCCCTGTGGCAGGC"
|
| 90 |
+
"TGGCAACCCCAGAGAAGTACCTGTTCAAGAACGGCGACCAGCGGCCCAACAAGGAGATCCCCA"
|
| 91 |
+
"AGAGCATCATCCTGGAGGAGTTCAAGGCCTTCTTCTCCACCTTCATCAACCGGAAGATGATCA"
|
| 92 |
+
"AGCAGACCGACAAAGACCAGGTGATCAGCCTGGGCGGCAAGGACCAGGTGCTGATCCAGATGC"
|
| 93 |
+
"AGCCCCAGGTGAGCAAGGACTTTGGCTTCAGCCTGTGCACCTGCCCCTGGGGCCACCCCAGCC"
|
| 94 |
+
"CCTGCAGCAGCACATCCTGTACTTCCTGAACCAGAAGGCCAAACAGTTCCTGCTGCAGGACGAG"
|
| 95 |
+
"AAGGTGAAGGGCATCAACCACTGCAAGGTGCGGGTGGCCCTGGAGCAGGACGGCAGCAAGGTG"
|
| 96 |
+
)
|
| 97 |
+
CDS_FIX = CDS_FIX[:len(CDS_FIX) - len(CDS_FIX) % 3]
|
| 98 |
+
CDS_FIX = CDS_FIX[:-3] + "TAA"
|
| 99 |
+
|
| 100 |
+
FULL_MRNA_EGFP = UTR5_BETAGLOBIN + KOZAK_TEV + "ATG" + CDS_EGFP[3:] + UTR3_BETAGLOBIN + POLYA_120
|
| 101 |
+
FULL_MRNA_MCHERRY = UTR5_EMCV + CDS_MCHERRY + UTR3_ALBUMIN + POLYA_60
|
| 102 |
+
|
| 103 |
+
# Build CSV rows
|
| 104 |
+
CSV_DATA = [
|
| 105 |
+
{
|
| 106 |
+
'id': 1,
|
| 107 |
+
'gene_name': 'eGFP-hBG-UTRs',
|
| 108 |
+
'five_prime_utr': UTR5_BETAGLOBIN,
|
| 109 |
+
'cds': CDS_EGFP,
|
| 110 |
+
'three_prime_utr': UTR3_BETAGLOBIN,
|
| 111 |
+
'poly_a_tail': POLYA_120,
|
| 112 |
+
'full_mrna': '',
|
| 113 |
+
'target_protein': 'Enhanced GFP',
|
| 114 |
+
'organism': 'Aequorea victoria',
|
| 115 |
+
'expression_system': 'HEK293T',
|
| 116 |
+
'gc_target_percent': 52.4,
|
| 117 |
+
'notes': 'Human beta-globin UTRs. Classic reporter construct for mRNA transfection.'
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
'id': 2,
|
| 121 |
+
'gene_name': 'mCherry-AlbUTR',
|
| 122 |
+
'five_prime_utr': UTR5_BETAGLOBIN,
|
| 123 |
+
'cds': CDS_MCHERRY,
|
| 124 |
+
'three_prime_utr': UTR3_ALBUMIN,
|
| 125 |
+
'poly_a_tail': POLYA_120,
|
| 126 |
+
'full_mrna': '',
|
| 127 |
+
'target_protein': 'mCherry red fluorescent protein',
|
| 128 |
+
'organism': 'Discosoma sp.',
|
| 129 |
+
'expression_system': 'CHO',
|
| 130 |
+
'gc_target_percent': 50.1,
|
| 131 |
+
'notes': 'Albumin 3\'UTR for extended expression. Good cell viability.'
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
'id': 3,
|
| 135 |
+
'gene_name': 'Luc2-reporter',
|
| 136 |
+
'five_prime_utr': UTR5_EMCV,
|
| 137 |
+
'cds': CDS_LUC2,
|
| 138 |
+
'three_prime_utr': UTR3_ALBUMIN,
|
| 139 |
+
'poly_a_tail': POLYA_60,
|
| 140 |
+
'full_mrna': '',
|
| 141 |
+
'target_protein': 'Firefly luciferase',
|
| 142 |
+
'organism': 'Photinus pyralis',
|
| 143 |
+
'expression_system': 'Huh-7',
|
| 144 |
+
'gc_target_percent': 53.8,
|
| 145 |
+
'notes': 'Bioluminescence reporter. Used for LNP screening.'
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
'id': 4,
|
| 149 |
+
'gene_name': 'SpRBD-v1',
|
| 150 |
+
'five_prime_utr': UTR5_BETAGLOBIN,
|
| 151 |
+
'cds': CDS_SPIKE_RBD,
|
| 152 |
+
'three_prime_utr': UTR3_ALBUMIN,
|
| 153 |
+
'poly_a_tail': POLYA_120,
|
| 154 |
+
'full_mrna': '',
|
| 155 |
+
'target_protein': 'SARS-CoV-2 Spike RBD',
|
| 156 |
+
'organism': 'SARS-CoV-2',
|
| 157 |
+
'expression_system': 'HEK293T',
|
| 158 |
+
'gc_target_percent': 55.2,
|
| 159 |
+
'notes': 'Vaccine antigen candidate. Proline-stabilized RBD.'
|
| 160 |
+
},
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
+
def create_csv():
|
| 164 |
+
output_path = os.path.join(os.path.dirname(__file__), 'mrna_sequences.csv')
|
| 165 |
+
|
| 166 |
+
with open(output_path, 'w', newline='') as f:
|
| 167 |
+
fieldnames = ['id', 'gene_name', 'five_prime_utr', 'cds', 'three_prime_utr',
|
| 168 |
+
'poly_a_tail', 'full_mrna', 'target_protein', 'organism',
|
| 169 |
+
'expression_system', 'gc_target_percent', 'notes']
|
| 170 |
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
| 171 |
+
writer.writeheader()
|
| 172 |
+
writer.writerows(CSV_DATA)
|
| 173 |
+
|
| 174 |
+
print(f'β Created {output_path}')
|
| 175 |
+
print(f' {len(CSV_DATA)} sequences exported')
|
| 176 |
+
return output_path
|
| 177 |
+
|
| 178 |
+
if __name__ == '__main__':
|
| 179 |
+
create_csv()
|
demo/create_demo_db.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Creates the demo SQLite database: demo/mrna_parts.db
|
| 3 |
+
|
| 4 |
+
Tables
|
| 5 |
+
------
|
| 6 |
+
mrna_sequences β main parts registry (mix of full mRNA and component records)
|
| 7 |
+
plasmid_backbones β a few cloning vectors
|
| 8 |
+
|
| 9 |
+
Run: python demo/create_demo_db.py
|
| 10 |
+
"""
|
| 11 |
+
import sqlite3
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
DB_PATH = os.path.join(os.path.dirname(__file__), "mrna_parts.db")
|
| 15 |
+
|
| 16 |
+
# ββ Sequences are realistic-length DNA (T not U), all valid reading frames ββ
|
| 17 |
+
|
| 18 |
+
# Human beta-globin 5'UTR (commonly used in mRNA therapeutics)
|
| 19 |
+
UTR5_BETAGLOBIN = (
|
| 20 |
+
"ACATTTGCTTCTGACACAACTGTGTTCACTAGCAACCTCAAACAGACACCATGGTGCATCTGAC"
|
| 21 |
+
"TCCTGAGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGGTGAACGTGGATGAAGTTGGTGGT"
|
| 22 |
+
)[:80]
|
| 23 |
+
|
| 24 |
+
# EMCV IRES-derived 5'UTR (used in bicistronic vectors)
|
| 25 |
+
UTR5_EMCV = (
|
| 26 |
+
"GGGAAATAAGAGAGAAAAGAAGAGTAAGAAGAAATATAAGAGCCACCATG"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Tobacco etch virus (TEV) minimal Kozak context
|
| 30 |
+
KOZAK_TEV = "GCCACC"
|
| 31 |
+
|
| 32 |
+
# Human beta-globin 3'UTR
|
| 33 |
+
UTR3_BETAGLOBIN = (
|
| 34 |
+
"GCTCGCTTTCTTGCTGTCCAATTTCTATTAAAGGTTCCTTTGTTCCCTAAGTCCAACTACTA"
|
| 35 |
+
"AACTGGGGGATATTATGAAGGGCCTTGAGCATCTGGATTCTGCCTAATAAAAAACATTTATT"
|
| 36 |
+
"TTCATTGCAATGATGTATTTAAATTATTTCTGAATATTTTACTAAAAATAAATGTTTTTTAT"
|
| 37 |
+
)[:100]
|
| 38 |
+
|
| 39 |
+
# Human albumin 3'UTR (strong, widely used)
|
| 40 |
+
UTR3_ALBUMIN = (
|
| 41 |
+
"AATAAAGATCTTTATTTTCATTAGATCTGTGTGTTGGTTTTTTGTGTGAATCGATAGTACTA"
|
| 42 |
+
"AATACTTTTCAGACACCAGAAATGCAGAGCAGTTCAGAGGCAGAGCCATCTATTGCTTACAT"
|
| 43 |
+
)[:100]
|
| 44 |
+
|
| 45 |
+
# Poly-A tail
|
| 46 |
+
POLYA_120 = "A" * 120
|
| 47 |
+
POLYA_60 = "A" * 60
|
| 48 |
+
|
| 49 |
+
# ββ CDS sequences β all start ATG, end stop codon, in-frame ββββββββββββββββββ
|
| 50 |
+
|
| 51 |
+
# eGFP (enhanced Green Fluorescent Protein) β 720 nt human-codon-optimized
|
| 52 |
+
CDS_EGFP = (
|
| 53 |
+
"ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCCTGACCTACGGCGTGCAGTGCTTCAGCCGCTACCCCGACCACATGAAGCAGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCACCCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAG"
|
| 54 |
+
"TAA" # stop codon
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# mCherry CDS β 711 nt, codon-optimized for human
|
| 58 |
+
CDS_MCHERRY = (
|
| 59 |
+
"ATGGTGAGCAAGGGCGAGGAGGATAACATGGCCATCATCAAGGAGTTCATGCGCTTCAAGGTGCACATGGAGGGCTCCGTGAACGGCCACGAGTTCGAGATCGAGGGCGAGGGCGAGGGCCGCCCCTACGAGGGCACCCAGACCGCCAAGCTGAAGGTGACCAAGGGTGGCCCCCTGCCCTTCGCCTGGGACATCCTGTCCCCTCAGTTCATGTACGGCTCCAAGGCCTACGTGAAGCACCCCGCCGACATCCCCGACTACTTGAAGCTGTCCTTCCCCGAGGGCTTCAAGTGGGAGCGCGTGATGAACTTCGAGGACGGCGGCGTGGTGACCGTGACCCAGGACTCCTCCCTGCAGGACGGCGAGTTCATCTACAAGGTGAAGCTGCGCGGCACCAACTTCCCCTCCGACGGCCCCGTAATGCAGAAGAAGACCATGGGCTGGGAGGCCTCCTCCGAGCGGATGTACCCCGAGGACGGCGCCCTGAAGGGCGAGATCAAGCAGAGGCTGAAGCTGAAGGACGGCGGCCACTACGACGCTGAGGTCAAGACCACCTACAAGGCCAAGAAGCCCGTGCAGCTGCCCGGCGCCTACAACGTCAACATCAAGTTGGACATCACCTCCCACAACGAGGACTACACCATCGTGGAACAGTACGAACGCGCCGAGGGCCGCCACTCCACCGGCGGCATGGACGAGCTGTACAAG"
|
| 60 |
+
"TAA"
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# Firefly luciferase β human codon-optimized excerpt (900 nt out of full ~1650)
|
| 64 |
+
CDS_LUC2 = (
|
| 65 |
+
"ATGGAAGATGCCAAAAACATTAAGAAGGGCCCAGCGCCATTCTACCCACTCGAAGACGGGAC"
|
| 66 |
+
"CGCCGGCGAGCAGCTGCACAAAGCCATGAAGCGCTACGCCCTGGTGCCCGGCACCATCGCCT"
|
| 67 |
+
"TTACCGACGCACATATCGAGGTGGACATTACCTACGCCGAGTACTTCGAGATGAGCGTTCGG"
|
| 68 |
+
"CTGGCAGAAGCTATGAAGCGCTATGGGCTGAATACAAACCATCGGATCGTGGTGTGCAGCGA"
|
| 69 |
+
"GAATAGTCTGGAGAAGATCCTGCTGAACAAAGGCCTGCCTGTAGCCGGCCTTTTCCTCCTGG"
|
| 70 |
+
"AAGAGCTGCGGCAGCAGTTCCAGAAGGCCCGGGAGCAGATGTTCACCTTCGTGCTCGATCTG"
|
| 71 |
+
"GAGGAAATGACCGCCGAAGAGGCGATTGAGAATCTGGTATTCGAGCAGTATGGAATCGACCA"
|
| 72 |
+
"TTATCTTGATAACCCACAATGCCTGCATGACCTGGTGCATCTGGAACCCCGAGGTCAATGTG"
|
| 73 |
+
"GAAGAGTTCCTGGAAAAGCTGCTGAAGGACGGTATCATCATGTTCAGCATCCATGGTTATGG"
|
| 74 |
+
"CTACATCCTGGGGCCCGGAACCAACTTCGATCTGGAGCGCATGATCAAGCGCGATGGGGAG"
|
| 75 |
+
"GTGGATATGGCCCTGATTAAGGTGTCGATGGAGCAGGCCGGCATCGACCCCGATGAGGCCGG"
|
| 76 |
+
"AGCCATTCGGCTGTACAAGCTGATGAAGGATAAG"
|
| 77 |
+
"TAA"
|
| 78 |
+
)[:900]
|
| 79 |
+
# Pad to make divisible by 3
|
| 80 |
+
while len(CDS_LUC2) % 3 != 0:
|
| 81 |
+
CDS_LUC2 = CDS_LUC2[:-3] + "TAA"
|
| 82 |
+
|
| 83 |
+
# Truncated Spike RBD (SARS-CoV-2) β synthetic, codon-optimized excerpt
|
| 84 |
+
CDS_SPIKE_RBD = (
|
| 85 |
+
"ATGTTCGTGTTCCTGGTGCTGCTGCCCCTGGTGTCCTCCCAGGTGTGCAACCTGACCACCAG"
|
| 86 |
+
"AACCCAGCTGCCCCCCGCCTACACCAACTCCTTCACCCGGGGCGTGTACTACCCCGACAAGG"
|
| 87 |
+
"TGTTCCGCTCCTCCGTGCTGCACTCCACCCAGGACCTGTTTCTGCCCTTTTTCTCCAACGTG"
|
| 88 |
+
"ACCTGGTTCCACGCCATCCACGTGTCCGGCACCAACGGCACAAAGCGGTTCGACAACCCCGTG"
|
| 89 |
+
"CTGCCCTTCAACGACGGGGTGTACTTTGCCAGCACCGAGAAGTCCAACATCATCCGGGGCTG"
|
| 90 |
+
"GATCTTCGGCACCACCCTGGACTCCAAGACCCAGTCCCTGCTGATCGTGAACAACGCCACCA"
|
| 91 |
+
"ACGTGGTCATCAAGGTGTGCGAGTTCCAGTTCTGCAACGACCCCTTCCTGGGCGTCTACTAC"
|
| 92 |
+
"CACAAGAACAACAAGTCCTGGATGGAGTCCGAGTTCCGGGTGTACTCCTCCGCCAACAACTG"
|
| 93 |
+
"CACCTTTGAGTACGTGTCCCAGCCCTTTCTGATGGACCTGGAGGGCAAACAGGGCAACTTCA"
|
| 94 |
+
"AGAACCTGCGCGAGTTTGTGTTTAAGAACATCGACGGCTACTTCAAGATCTACAGCAAGCAC"
|
| 95 |
+
)
|
| 96 |
+
# Ensure divisible by 3 with stop codon
|
| 97 |
+
CDS_SPIKE_RBD = CDS_SPIKE_RBD[:len(CDS_SPIKE_RBD) - len(CDS_SPIKE_RBD) % 3]
|
| 98 |
+
CDS_SPIKE_RBD = CDS_SPIKE_RBD[:-3] + "TAA" # replace last codon with stop
|
| 99 |
+
|
| 100 |
+
# EPO (Erythropoietin) CDS β synthetic excerpt
|
| 101 |
+
CDS_EPO = (
|
| 102 |
+
"ATGGGGGTGCACGAATGTCCCGCCTGGCTGTGGCTGCTGCTGTCGCTGCCGTTCTCTGTGCT"
|
| 103 |
+
"GCCCGCCCGCGCCGTCCTCACCGTCAACTTCCCGCACCCTGCTTCCACGCCTCAGAGTCCTG"
|
| 104 |
+
"GAGAGGTACCTCTTGGAGGCCAAGGAGGCCGAGAATATCACGACGGGCTGTGCTGAACACTGC"
|
| 105 |
+
"AGCTTGAATGAGAATATCACGGTGCGCTTTCCACGCCTCATTTGCGACAGCTTTGTTCGTGG"
|
| 106 |
+
"TCAGGCCGTGGTCAGCTCCGATGAGGTCTTCAGGGCCCCTGTCCTCCTGCAGCTGGAATCCT"
|
| 107 |
+
"GGCAGCGTCTCAGCCCCTGCAGCCAGCCCTCCCAGCTGCCCTCAGCCACCTGTCCCGCCTGCT"
|
| 108 |
+
"CCAGAGCCTGGAGAACTTCTACCAGCCTCTGGAGCAGCTCCAGGAAGTGATCCAGGAGATGAG"
|
| 109 |
+
"CAAGCTGTCCGCCACGGCCGTGGAGGTCTTGGCCAGTAAGCCGGAG"
|
| 110 |
+
"TAA"
|
| 111 |
+
)
|
| 112 |
+
# Truncate to multiples of 3
|
| 113 |
+
CDS_EPO = CDS_EPO[:len(CDS_EPO) - len(CDS_EPO) % 3]
|
| 114 |
+
if not CDS_EPO.endswith("TAA") and not CDS_EPO.endswith("TAG") and not CDS_EPO.endswith("TGA"):
|
| 115 |
+
CDS_EPO = CDS_EPO[:-3] + "TAA"
|
| 116 |
+
|
| 117 |
+
# hFIX (Factor IX) β synthetic
|
| 118 |
+
CDS_FIX = (
|
| 119 |
+
"ATGCAGCGCGTGAACATGATCATGGCCTCCCTGTGGCTGTGCTTCGTGGCCCTGTGGCAGGC"
|
| 120 |
+
"TGGCAACCCCAGAGAAGTACCTGTTCAAGAACGGCGACCAGCGGCCCAACAAGGAGATCCCCA"
|
| 121 |
+
"AGAGCATCATCCTGGAGGAGTTCAAGGCCTTCTTCTCCACCTTCATCAACCGGAAGATGATCA"
|
| 122 |
+
"AGCAGACCGACAAAGACCAGGTGATCAGCCTGGGCGGCAAGGACCAGGTGCTGATCCAGATGC"
|
| 123 |
+
"AGCCCCAGGTGAGCAAGGACTTTGGCTTCAGCCTGTGCACCTGCCCCTGGGGCCACCCCAGCC"
|
| 124 |
+
"CCTGCAGCAGCACATCCTGTACTTCCTGAACCAGAAGGCCAAACAGTTCCTGCTGCAGGACGAG"
|
| 125 |
+
"AAGGTGAAGGGCATCAACCACTGCAAGGTGCGGGTGGCCCTGGAGCAGGACGGCAGCAAGGTG"
|
| 126 |
+
)
|
| 127 |
+
CDS_FIX = CDS_FIX[:len(CDS_FIX) - len(CDS_FIX) % 3]
|
| 128 |
+
CDS_FIX = CDS_FIX[:-3] + "TAA"
|
| 129 |
+
|
| 130 |
+
# ββ Full mRNA sequences (some customers store it monolithic) ββββββββββββββββββ
|
| 131 |
+
|
| 132 |
+
FULL_MRNA_EGFP = UTR5_BETAGLOBIN + KOZAK_TEV + "ATG" + CDS_EGFP[3:] + UTR3_BETAGLOBIN + POLYA_120
|
| 133 |
+
FULL_MRNA_MCHERRY = UTR5_EMCV + CDS_MCHERRY + UTR3_ALBUMIN + POLYA_60
|
| 134 |
+
|
| 135 |
+
# ββ Sequences table data βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 136 |
+
|
| 137 |
+
SEQUENCES = [
|
| 138 |
+
# id, gene_name, five_prime_utr, cds, three_prime_utr, poly_a_tail,
|
| 139 |
+
# target_protein, organism, expression_system, gc_target, notes
|
| 140 |
+
(1, "eGFP-hBG-UTRs", UTR5_BETAGLOBIN, CDS_EGFP, UTR3_BETAGLOBIN, POLYA_120,
|
| 141 |
+
"Enhanced GFP", "Aequorea victoria", "HEK293T", 52.4,
|
| 142 |
+
"Human beta-globin UTRs. Classic reporter construct for mRNA transfection."),
|
| 143 |
+
|
| 144 |
+
(2, "mCherry-AlbUTR", UTR5_BETAGLOBIN, CDS_MCHERRY, UTR3_ALBUMIN, POLYA_120,
|
| 145 |
+
"mCherry red fluorescent protein", "Discosoma sp.", "CHO", 50.1,
|
| 146 |
+
"Albumin 3'UTR for extended expression. Good cell viability."),
|
| 147 |
+
|
| 148 |
+
(3, "Luc2-reporter", UTR5_EMCV, CDS_LUC2, UTR3_ALBUMIN, POLYA_60,
|
| 149 |
+
"Firefly luciferase", "Photinus pyralis", "Huh-7", 53.8,
|
| 150 |
+
"Bioluminescence reporter. Used for LNP screening."),
|
| 151 |
+
|
| 152 |
+
(4, "SpRBD-v1", UTR5_BETAGLOBIN, CDS_SPIKE_RBD, UTR3_ALBUMIN, POLYA_120,
|
| 153 |
+
"SARS-CoV-2 Spike RBD", "SARS-CoV-2", "HEK293T", 55.2,
|
| 154 |
+
"Vaccine antigen candidate. Proline-stabilized RBD."),
|
| 155 |
+
|
| 156 |
+
(5, "EPO-therapeutic", UTR5_BETAGLOBIN, CDS_EPO, UTR3_BETAGLOBIN, POLYA_120,
|
| 157 |
+
"Erythropoietin", "Homo sapiens", "HepG2", 58.0,
|
| 158 |
+
"Therapeutic candidate for anemia. Glycosylation motifs retained."),
|
| 159 |
+
|
| 160 |
+
(6, "hFIX-codon-opt", UTR5_BETAGLOBIN, CDS_FIX, UTR3_ALBUMIN, POLYA_120,
|
| 161 |
+
"Coagulation Factor IX", "Homo sapiens", "HepG2", 56.3,
|
| 162 |
+
"Hemophilia B gene therapy candidate. Codon-optimized for liver expression."),
|
| 163 |
+
|
| 164 |
+
# Records stored as full_mrna (no component breakdown) β different schema
|
| 165 |
+
(7, "eGFP-full-v2", None, None, None, None,
|
| 166 |
+
"eGFP monolithic record", "Aequorea victoria", "in vitro", 51.9,
|
| 167 |
+
"Archive record β stored as assembled mRNA sequence."),
|
| 168 |
+
|
| 169 |
+
(8, "mCherry-EMCV-full", None, None, None, None,
|
| 170 |
+
"mCherry EMCV", "Discosoma sp.", "Jurkat", 49.7,
|
| 171 |
+
"EMCV IRES-driven construct. Full sequence stored."),
|
| 172 |
+
]
|
| 173 |
+
|
| 174 |
+
# full_mrna is only for rows 7 and 8
|
| 175 |
+
FULL_MRNA = {
|
| 176 |
+
7: FULL_MRNA_EGFP,
|
| 177 |
+
8: FULL_MRNA_MCHERRY,
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
# ββ Plasmid backbones βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 181 |
+
|
| 182 |
+
# pUC19 minimal cloning region (stub β real pUC19 is 2686 bp)
|
| 183 |
+
BACKBONE_PUC19 = "ATGACCATGATTACGCCAAGCTTGCATGCCTGCAGGTCGACGGATCCCCGGGAATTCGAGCTC" + "GCTAGC" * 40
|
| 184 |
+
BACKBONE_CMV = "GGTACCGAGCTCGAATTCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCT" + "AAGCTT" * 40
|
| 185 |
+
|
| 186 |
+
BACKBONES = [
|
| 187 |
+
(1, "pUC19-MCS", BACKBONE_PUC19,
|
| 188 |
+
"EcoRI,HindIII,BamHI,SalI,PstI,SphI",
|
| 189 |
+
"Classic E. coli cloning vector. AmpR. lacZ alpha complementation."),
|
| 190 |
+
|
| 191 |
+
(2, "pCMV-MCS", BACKBONE_CMV,
|
| 192 |
+
"EcoRI,HindIII,XhoI,NheI,NotI",
|
| 193 |
+
"Mammalian expression vector. CMV promoter. BGH poly-A signal."),
|
| 194 |
+
]
|
| 195 |
+
|
| 196 |
+
# ββ Build the database βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 197 |
+
|
| 198 |
+
def create() -> None:
|
| 199 |
+
if os.path.exists(DB_PATH):
|
| 200 |
+
os.remove(DB_PATH)
|
| 201 |
+
|
| 202 |
+
conn = sqlite3.connect(DB_PATH)
|
| 203 |
+
c = conn.cursor()
|
| 204 |
+
|
| 205 |
+
c.execute("""
|
| 206 |
+
CREATE TABLE mrna_sequences (
|
| 207 |
+
id INTEGER PRIMARY KEY,
|
| 208 |
+
gene_name TEXT NOT NULL,
|
| 209 |
+
five_prime_utr TEXT,
|
| 210 |
+
cds TEXT,
|
| 211 |
+
three_prime_utr TEXT,
|
| 212 |
+
poly_a_tail TEXT,
|
| 213 |
+
full_mrna TEXT,
|
| 214 |
+
target_protein TEXT,
|
| 215 |
+
organism TEXT,
|
| 216 |
+
expression_system TEXT,
|
| 217 |
+
gc_target_percent REAL,
|
| 218 |
+
notes TEXT
|
| 219 |
+
)
|
| 220 |
+
""")
|
| 221 |
+
|
| 222 |
+
for row in SEQUENCES:
|
| 223 |
+
(rid, name, utr5, cds, utr3, polya,
|
| 224 |
+
protein, organism, expsys, gc, notes) = row
|
| 225 |
+
full = FULL_MRNA.get(rid)
|
| 226 |
+
c.execute("""
|
| 227 |
+
INSERT INTO mrna_sequences VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
| 228 |
+
""", (rid, name, utr5, cds, utr3, polya, full,
|
| 229 |
+
protein, organism, expsys, gc, notes))
|
| 230 |
+
|
| 231 |
+
c.execute("""
|
| 232 |
+
CREATE TABLE plasmid_backbones (
|
| 233 |
+
id INTEGER PRIMARY KEY,
|
| 234 |
+
name TEXT,
|
| 235 |
+
sequence TEXT,
|
| 236 |
+
cloning_sites TEXT,
|
| 237 |
+
description TEXT
|
| 238 |
+
)
|
| 239 |
+
""")
|
| 240 |
+
|
| 241 |
+
for row in BACKBONES:
|
| 242 |
+
c.execute("INSERT INTO plasmid_backbones VALUES (?,?,?,?,?)", row)
|
| 243 |
+
|
| 244 |
+
conn.commit()
|
| 245 |
+
conn.close()
|
| 246 |
+
|
| 247 |
+
print(f"β Created demo database: {DB_PATH}")
|
| 248 |
+
print(f" mrna_sequences : {len(SEQUENCES)} records")
|
| 249 |
+
print(f" plasmid_backbones: {len(BACKBONES)} records")
|
| 250 |
+
|
| 251 |
+
# Quick validation
|
| 252 |
+
conn2 = sqlite3.connect(DB_PATH)
|
| 253 |
+
count = conn2.execute("SELECT COUNT(*) FROM mrna_sequences").fetchone()[0]
|
| 254 |
+
conn2.close()
|
| 255 |
+
assert count == len(SEQUENCES), "Row count mismatch!"
|
| 256 |
+
print(" β Validation passed")
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
if __name__ == "__main__":
|
| 260 |
+
create()
|
demo/demo_models.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Demo script for mRNA scoring models.
|
| 3 |
+
|
| 4 |
+
Shows how to use the RNAstructure MFE and mRNA Stability scorers.
|
| 5 |
+
"""
|
| 6 |
+
from core.models.sequence import mRNASequence
|
| 7 |
+
from models import RNAStructureMFEScorer, mRNAStabilityScorer, ModelRegistry
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def demo_individual_scorers():
|
| 11 |
+
"""Demo individual model scoring."""
|
| 12 |
+
print("=" * 60)
|
| 13 |
+
print("Demo: Individual Model Scoring")
|
| 14 |
+
print("=" * 60)
|
| 15 |
+
|
| 16 |
+
# Create a test mRNA sequence
|
| 17 |
+
seq = mRNASequence(
|
| 18 |
+
name="EGFP_construct",
|
| 19 |
+
source="local",
|
| 20 |
+
five_prime_utr="GTTGCTCCTTCGGGCCTGTGGCGGCT",
|
| 21 |
+
kozak="GCCACCATGG",
|
| 22 |
+
cds="ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAG",
|
| 23 |
+
three_prime_utr="TGCCTGCTGCCGAGCGCCTGCGCGCGCGCGAG",
|
| 24 |
+
poly_a="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
print(f"\nSequence: {seq.name}")
|
| 28 |
+
print(f"Length: {seq.length} nt")
|
| 29 |
+
print(f"Components: 5'UTR, Kozak, CDS, 3'UTR, PolyA\n")
|
| 30 |
+
|
| 31 |
+
# Score with RNAstructure MFE
|
| 32 |
+
print("-" * 60)
|
| 33 |
+
print("RNAstructure MFE Scorer")
|
| 34 |
+
print("-" * 60)
|
| 35 |
+
mfe_scorer = RNAStructureMFEScorer()
|
| 36 |
+
mfe_score = mfe_scorer.score(seq)
|
| 37 |
+
print(f"Score: {mfe_score:.1f}/100")
|
| 38 |
+
print(f"Interpretation: ", end="")
|
| 39 |
+
if mfe_score < 40:
|
| 40 |
+
print("Weak secondary structure")
|
| 41 |
+
elif mfe_score < 70:
|
| 42 |
+
print("Optimal structure for translation β")
|
| 43 |
+
else:
|
| 44 |
+
print("Strong structure (may inhibit translation)")
|
| 45 |
+
|
| 46 |
+
# Score with mRNA Stability
|
| 47 |
+
print("\n" + "-" * 60)
|
| 48 |
+
print("mRNA Stability Scorer")
|
| 49 |
+
print("-" * 60)
|
| 50 |
+
stability_scorer = mRNAStabilityScorer(organism="human")
|
| 51 |
+
stability_score = stability_scorer.score(seq)
|
| 52 |
+
print(f"Overall Score: {stability_score:.1f}/100")
|
| 53 |
+
print(f"Interpretation: ", end="")
|
| 54 |
+
if stability_score >= 70:
|
| 55 |
+
print("Excellent design β")
|
| 56 |
+
elif stability_score >= 40:
|
| 57 |
+
print("Acceptable design")
|
| 58 |
+
else:
|
| 59 |
+
print("Poor design, optimization recommended")
|
| 60 |
+
|
| 61 |
+
# Show component scores
|
| 62 |
+
print("\n Component Breakdown:")
|
| 63 |
+
gc_score = stability_scorer._score_gc_content(seq)
|
| 64 |
+
cai_score = stability_scorer._score_cai(seq)
|
| 65 |
+
homopoly_score = stability_scorer._score_homopolymers(seq)
|
| 66 |
+
utr_score = stability_scorer._score_utr_structure(seq)
|
| 67 |
+
kozak_score = stability_scorer._score_kozak(seq)
|
| 68 |
+
|
| 69 |
+
if gc_score is not None:
|
| 70 |
+
print(f" GC Content (30%): {gc_score:.1f}/100")
|
| 71 |
+
if cai_score is not None:
|
| 72 |
+
print(f" CAI (25%): {cai_score:.1f}/100")
|
| 73 |
+
if homopoly_score is not None:
|
| 74 |
+
print(f" Homopolymers (20%): {homopoly_score:.1f}/100")
|
| 75 |
+
if utr_score is not None:
|
| 76 |
+
print(f" 5' UTR (15%): {utr_score:.1f}/100")
|
| 77 |
+
if kozak_score is not None:
|
| 78 |
+
print(f" Kozak (10%): {kozak_score:.1f}/100")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def demo_model_registry():
|
| 82 |
+
"""Demo ModelRegistry integration."""
|
| 83 |
+
print("\n\n" + "=" * 60)
|
| 84 |
+
print("Demo: ModelRegistry Integration")
|
| 85 |
+
print("=" * 60)
|
| 86 |
+
|
| 87 |
+
# Create sequences
|
| 88 |
+
sequences = [
|
| 89 |
+
mRNASequence(
|
| 90 |
+
name="seq_good",
|
| 91 |
+
source="local",
|
| 92 |
+
five_prime_utr="GTTGCTCCTTCGGGCCTGTGGCGGCT",
|
| 93 |
+
kozak="GCCACCATGG",
|
| 94 |
+
cds="ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGG" * 2,
|
| 95 |
+
),
|
| 96 |
+
mRNASequence(
|
| 97 |
+
name="seq_poor",
|
| 98 |
+
source="local",
|
| 99 |
+
cds="ATGAAAAAAAAAAAAAAAAAAAAAAATGA", # Poor: homopolymers, low GC
|
| 100 |
+
),
|
| 101 |
+
mRNASequence(
|
| 102 |
+
name="seq_medium",
|
| 103 |
+
source="local",
|
| 104 |
+
five_prime_utr="ACGTACGT",
|
| 105 |
+
cds="ATGCGACGATCGATCGATCGACGATGA",
|
| 106 |
+
),
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
# Create registry
|
| 110 |
+
registry = ModelRegistry()
|
| 111 |
+
|
| 112 |
+
# Register models
|
| 113 |
+
registry._register(RNAStructureMFEScorer(), "scoring", "builtin", "")
|
| 114 |
+
registry._register(mRNAStabilityScorer(), "scoring", "builtin", "")
|
| 115 |
+
|
| 116 |
+
print(f"\nRegistered models: {len(registry.scoring_models)}")
|
| 117 |
+
for model in registry.scoring_models:
|
| 118 |
+
print(f" - {model.model.name}")
|
| 119 |
+
|
| 120 |
+
# Score all sequences with both models
|
| 121 |
+
print("\n" + "-" * 60)
|
| 122 |
+
print("Scoring Results")
|
| 123 |
+
print("-" * 60)
|
| 124 |
+
|
| 125 |
+
for model_reg in registry.scoring_models:
|
| 126 |
+
model_name = model_reg.model.name
|
| 127 |
+
print(f"\n{model_name}:")
|
| 128 |
+
results = registry.run_scoring(model_name, sequences)
|
| 129 |
+
for _, row in results.iterrows():
|
| 130 |
+
print(f" {row['name']:15} β {row['score']:5.1f}")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def demo_batch_scoring():
|
| 134 |
+
"""Demo batch scoring efficiency."""
|
| 135 |
+
print("\n\n" + "=" * 60)
|
| 136 |
+
print("Demo: Batch Scoring")
|
| 137 |
+
print("=" * 60)
|
| 138 |
+
|
| 139 |
+
# Create 10 test sequences
|
| 140 |
+
sequences = [
|
| 141 |
+
mRNASequence(
|
| 142 |
+
name=f"seq_{i:02d}",
|
| 143 |
+
source="local",
|
| 144 |
+
cds="ATGCGATCGATCGATCG" * (i + 1),
|
| 145 |
+
)
|
| 146 |
+
for i in range(10)
|
| 147 |
+
]
|
| 148 |
+
|
| 149 |
+
scorer = mRNAStabilityScorer()
|
| 150 |
+
|
| 151 |
+
# Batch score
|
| 152 |
+
scores = scorer.score_batch(sequences)
|
| 153 |
+
|
| 154 |
+
print(f"\nScored {len(sequences)} sequences:")
|
| 155 |
+
for seq, score in zip(sequences, scores):
|
| 156 |
+
print(f" {seq.name}: {score:.1f}/100 ({seq.length} nt)")
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
if __name__ == "__main__":
|
| 160 |
+
demo_individual_scorers()
|
| 161 |
+
demo_model_registry()
|
| 162 |
+
demo_batch_scoring()
|
| 163 |
+
|
| 164 |
+
print("\n" + "=" * 60)
|
| 165 |
+
print("Demo Complete!")
|
| 166 |
+
print("=" * 60)
|
demo/docker-compose.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
postgres:
|
| 5 |
+
image: postgres:16-alpine
|
| 6 |
+
container_name: mrna_studio_demo_db
|
| 7 |
+
environment:
|
| 8 |
+
POSTGRES_DB: mrna_studio
|
| 9 |
+
POSTGRES_USER: demo_user
|
| 10 |
+
POSTGRES_PASSWORD: demo_pass_2024
|
| 11 |
+
ports:
|
| 12 |
+
- "5432:5432"
|
| 13 |
+
volumes:
|
| 14 |
+
- ./init_postgres.sql:/docker-entrypoint-initdb.d/init.sql
|
| 15 |
+
healthcheck:
|
| 16 |
+
test: ["CMD-SHELL", "pg_isready -U demo_user -d mrna_studio"]
|
| 17 |
+
interval: 5s
|
| 18 |
+
timeout: 5s
|
| 19 |
+
retries: 5
|
demo/init_postgres.sql
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- mRNA Design Studio Demo Database
|
| 2 |
+
-- PostgreSQL initialization script
|
| 3 |
+
|
| 4 |
+
-- Create sequences table
|
| 5 |
+
CREATE TABLE mrna_sequences (
|
| 6 |
+
id SERIAL PRIMARY KEY,
|
| 7 |
+
gene_name VARCHAR(255) NOT NULL,
|
| 8 |
+
five_prime_utr TEXT,
|
| 9 |
+
cds TEXT,
|
| 10 |
+
three_prime_utr TEXT,
|
| 11 |
+
poly_a_tail TEXT,
|
| 12 |
+
full_mrna TEXT,
|
| 13 |
+
target_protein VARCHAR(255),
|
| 14 |
+
organism VARCHAR(255),
|
| 15 |
+
expression_system VARCHAR(255),
|
| 16 |
+
gc_target_percent DECIMAL(5,2),
|
| 17 |
+
notes TEXT,
|
| 18 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 19 |
+
);
|
| 20 |
+
|
| 21 |
+
-- Insert demo sequences
|
| 22 |
+
INSERT INTO mrna_sequences (gene_name, five_prime_utr, cds, three_prime_utr, poly_a_tail, full_mrna, target_protein, organism, expression_system, gc_target_percent, notes) VALUES
|
| 23 |
+
('eGFP-hBG-UTRs',
|
| 24 |
+
'ACATTTGCTTCTGACACAACTGTGTTCACTAGCAACCTCAAACAGACACCATGGTGCATCTGACTCCTGAGGAGAAG',
|
| 25 |
+
'ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCCTGACCTACGGCGTGCAGTGCTTCAGCCGCTACCCCGACCACATGAAGCAGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCACCCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAA',
|
| 26 |
+
'GCTCGCTTTCTTGCTGTCCAATTTCTATTAAAGGTTCCTTTGTTCCCTAAGTCCAACTACTAAACTGGGGGATATTATGAAGGGCCTTGAGCATCTGGA',
|
| 27 |
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
| 28 |
+
NULL,
|
| 29 |
+
'Enhanced GFP',
|
| 30 |
+
'Aequorea victoria',
|
| 31 |
+
'HEK293T',
|
| 32 |
+
52.4,
|
| 33 |
+
'Human beta-globin UTRs. Classic reporter construct for mRNA transfection.'),
|
| 34 |
+
|
| 35 |
+
('mCherry-AlbUTR',
|
| 36 |
+
'ACATTTGCTTCTGACACAACTGTGTTCACTAGCAACCTCAAACAGACACCATGGTGCATCTGACTCCTGAGGAGAAG',
|
| 37 |
+
'ATGGTGAGCAAGGGCGAGGAGGATAACATGGCCATCATCAAGGAGTTCATGCGCTTCAAGGTGCACATGGAGGGCTCCGTGAACGGCCACGAGTTCGAGATCGAGGGCGAGGGCGAGGGCCGCCCCTACGAGGGCACCCAGACCGCCAAGCTGAAGGTGACCAAGGGTGGCCCCCTGCCCTTCGCCTGGGACATCCTGTCCCCTCAGTTCATGTACGGCTCCAAGGCCTACGTGAAGCACCCCGCCGACATCCCCGACTACTTGAAGCTGTCCTTCCCCGAGGGCTTCAAGTGGGAGCGCGTGATGAACTTCGAGGACGGCGGCGTGGTGACCGTGACCCAGGACTCCTCCCTGCAGGACGGCGAGTTCATCTACAAGGTGAAGCTGCGCGGCACCAACTTCCCCTCCGACGGCCCCGTAATGCAGAAGAAGACCATGGGCTGGGAGGCCTCCTCCGAGCGGATGTACCCCGAGGACGGCGCCCTGAAGGGCGAGATCAAGCAGAGGCTGAAGCTGAAGGACGGCGGCCACTACGACGCTGAGGTCAAGACCACCTACAAGGCCAAGAAGCCCGTGCAGCTGCCCGGCGCCTACAACGTCAACATCAAGTTGGACATCACCTCCCACAACGAGGACTACACCATCGTGGAACAGTACGAACGCGCCGAGGGCCGCCACTCCACCGGCGGCATGGACGAGCTGTACAAGTAA',
|
| 38 |
+
'AATAAAGATCTTTATTTTCATTAGATCTGTGTGTTGGTTTTTTGTGTGAATCGATAGTACTAAATACTTTTCAGACACCAGAAATGCAGAGCAGTTCA',
|
| 39 |
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
| 40 |
+
NULL,
|
| 41 |
+
'mCherry red fluorescent protein',
|
| 42 |
+
'Discosoma sp.',
|
| 43 |
+
'CHO',
|
| 44 |
+
50.1,
|
| 45 |
+
'Albumin 3''UTR for extended expression. Good cell viability.'),
|
| 46 |
+
|
| 47 |
+
('eGFP-full-v2',
|
| 48 |
+
NULL, NULL, NULL, NULL,
|
| 49 |
+
'ACATTTGCTTCTGACACAACTGTGTTCACTAGCAACCTCAAACAGACACCATGGTGCATCTGACTCCTGAGGAGAAGGCCACCATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCCTGACCTACGGCGTGCAGTGCTTCAGCCGCTACCCCGACCACATGAAGCAGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCACCCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAGCTCGCTTTCTTGCTGTCCAATTTCTATTAAAGGTTCCTTTGTTCCCTAAGTCCAACTACTAAACTGGGGGATATTATGAAGGGCCTTGAGCATCTGGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
| 50 |
+
'eGFP monolithic record',
|
| 51 |
+
'Aequorea victoria',
|
| 52 |
+
'in vitro',
|
| 53 |
+
51.9,
|
| 54 |
+
'Archive record β stored as assembled mRNA sequence.'),
|
| 55 |
+
|
| 56 |
+
('mCherry-full',
|
| 57 |
+
NULL, NULL, NULL, NULL,
|
| 58 |
+
'GGGAAATAAGAGAGAAAAGAAGAGTAAGAAGAAATATAAGAGCCACCATGATGGTGAGCAAGGGCGAGGAGGATAACATGGCCATCATCAAGGAGTTCATGCGCTTCAAGGTGCACATGGAGGGCTCCGTGAACGGCCACGAGTTCGAGATCGAGGGCGAGGGCGAGGGCCGCCCCTACGAGGGCACCCAGACCGCCAAGCTGAAGGTGACCAAGGGTGGCCCCCTGCCCTTCGCCTGGGACATCCTGTCCCCTCAGTTCATGTACGGCTCCAAGGCCTACGTGAAGCACCCCGCCGACATCCCCGACTACTTGAAGCTGTCCTTCCCCGAGGGCTTCAAGTGGGAGCGCGTGATGAACTTCGAGGACGGCGGCGTGGTGACCGTGACCCAGGACTCCTCCCTGCAGGACGGCGAGTTCATCTACAAGGTGAAGCTGCGCGGCACCAACTTCCCCTCCGACGGCCCCGTAATGCAGAAGAAGACCATGGGCTGGGAGGCCTCCTCCGAGCGGATGTACCCCGAGGACGGCGCCCTGAAGGGCGAGATCAAGCAGAGGCTGAAGCTGAAGGACGGCGGCCACTACGACGCTGAGGTCAAGACCACCTACAAGGCCAAGAAGCCCGTGCAGCTGCCCGGCGCCTACAACGTCAACATCAAGTTGGACATCACCTCCCACAACGAGGACTACACCATCGTGGAACAGTACGAACGCGCCGAGGGCCGCCACTCCACCGGCGGCATGGACGAGCTGTACAAGTAAAATAAAGATCTTTATTTTCATTAGATCTGTGTGTTGGTTTTTTGTGTGAATCGATAGTACTAAATACTTTTCAGACACCAGAAATGCAGAGCAGTTCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
| 59 |
+
'mCherry EMCV',
|
| 60 |
+
'Discosoma sp.',
|
| 61 |
+
'Jurkat',
|
| 62 |
+
49.7,
|
| 63 |
+
'EMCV IRES-driven construct. Full sequence stored.');
|
| 64 |
+
|
| 65 |
+
-- Create plasmid backbones table
|
| 66 |
+
CREATE TABLE plasmid_backbones (
|
| 67 |
+
id SERIAL PRIMARY KEY,
|
| 68 |
+
name VARCHAR(255) NOT NULL,
|
| 69 |
+
sequence TEXT NOT NULL,
|
| 70 |
+
cloning_sites TEXT,
|
| 71 |
+
description TEXT,
|
| 72 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 73 |
+
);
|
| 74 |
+
|
| 75 |
+
-- Insert demo plasmid backbones
|
| 76 |
+
INSERT INTO plasmid_backbones (name, sequence, cloning_sites, description) VALUES
|
| 77 |
+
('pUC19-MCS',
|
| 78 |
+
'ATGACCATGATTACGCCAAGCTTGCATGCCTGCAGGTCGACGGATCCCCGGGAATTCGAGCTCGCTAGCGCTAGCGCTAGCGCTAGCGCTAGCGCTAGC',
|
| 79 |
+
'EcoRI,HindIII,BamHI,SalI,PstI',
|
| 80 |
+
'Classic E. coli cloning vector. AmpR. lacZ alpha complementation.');
|
| 81 |
+
|
| 82 |
+
-- Create indexes for better query performance
|
| 83 |
+
CREATE INDEX idx_mrna_gene_name ON mrna_sequences(gene_name);
|
| 84 |
+
CREATE INDEX idx_mrna_organism ON mrna_sequences(organism);
|
| 85 |
+
CREATE INDEX idx_plasmid_name ON plasmid_backbones(name);
|
| 86 |
+
|
| 87 |
+
-- Grant permissions
|
| 88 |
+
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO demo_user;
|
| 89 |
+
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO demo_user;
|
demo/init_railway_db.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Initialize the Railway PostgreSQL database with demo tables and data.
|
| 3 |
+
|
| 4 |
+
Reads connection details from environment variables (PGHOST, PGPORT, etc.)
|
| 5 |
+
or accepts a DATABASE_URL.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python demo/init_railway_db.py
|
| 9 |
+
"""
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
|
| 13 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
| 14 |
+
|
| 15 |
+
from sqlalchemy import create_engine, text
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_database_url() -> str:
|
| 19 |
+
"""Build a PostgreSQL URL from environment variables."""
|
| 20 |
+
url = os.environ.get("DATABASE_URL", "").strip()
|
| 21 |
+
if url:
|
| 22 |
+
return url
|
| 23 |
+
|
| 24 |
+
host = os.environ.get("PGHOST", "localhost")
|
| 25 |
+
port = os.environ.get("PGPORT", "5432")
|
| 26 |
+
db = os.environ.get("PGDATABASE", "railway")
|
| 27 |
+
user = os.environ.get("PGUSER", "postgres")
|
| 28 |
+
pw = os.environ.get("PGPASSWORD", "")
|
| 29 |
+
return f"postgresql+psycopg2://{user}:{pw}@{host}:{port}/{db}"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def main() -> None:
|
| 33 |
+
url = get_database_url()
|
| 34 |
+
safe_url = url.split("@")[-1] if "@" in url else url
|
| 35 |
+
print(f"Connecting to: ...@{safe_url}")
|
| 36 |
+
|
| 37 |
+
engine = create_engine(url)
|
| 38 |
+
|
| 39 |
+
sql_path = os.path.join(os.path.dirname(__file__), "init_postgres.sql")
|
| 40 |
+
with open(sql_path) as f:
|
| 41 |
+
sql = f.read()
|
| 42 |
+
|
| 43 |
+
# Remove the GRANT lines (Railway uses postgres user, not demo_user)
|
| 44 |
+
lines = [
|
| 45 |
+
line for line in sql.splitlines()
|
| 46 |
+
if not line.strip().startswith("GRANT ")
|
| 47 |
+
]
|
| 48 |
+
sql_clean = "\n".join(lines)
|
| 49 |
+
|
| 50 |
+
with engine.begin() as conn:
|
| 51 |
+
# Drop existing tables first (safe re-run)
|
| 52 |
+
conn.execute(text("DROP TABLE IF EXISTS mrna_sequences CASCADE"))
|
| 53 |
+
conn.execute(text("DROP TABLE IF EXISTS plasmid_backbones CASCADE"))
|
| 54 |
+
print("Dropped existing tables (if any)")
|
| 55 |
+
|
| 56 |
+
# Execute each statement
|
| 57 |
+
for statement in sql_clean.split(";"):
|
| 58 |
+
stmt = statement.strip()
|
| 59 |
+
if stmt and not stmt.startswith("--"):
|
| 60 |
+
conn.execute(text(stmt))
|
| 61 |
+
|
| 62 |
+
# Verify
|
| 63 |
+
with engine.connect() as conn:
|
| 64 |
+
seq_count = conn.execute(text("SELECT COUNT(*) FROM mrna_sequences")).scalar()
|
| 65 |
+
bb_count = conn.execute(text("SELECT COUNT(*) FROM plasmid_backbones")).scalar()
|
| 66 |
+
|
| 67 |
+
print(f"Done! Created:")
|
| 68 |
+
print(f" mrna_sequences: {seq_count} rows")
|
| 69 |
+
print(f" plasmid_backbones: {bb_count} rows")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
if __name__ == "__main__":
|
| 73 |
+
main()
|
demo/mrna_sequences.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
id,gene_name,five_prime_utr,cds,three_prime_utr,poly_a_tail,full_mrna,target_protein,organism,expression_system,gc_target_percent,notes
|
| 2 |
+
1,eGFP-hBG-UTRs,ACATTTGCTTCTGACACAACTGTGTTCACTAGCAACCTCAAACAGACACCATGGTGCATCTGACTCCTGAGGAGAAGTCT,ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCCTGACCTACGGCGTGCAGTGCTTCAGCCGCTACCCCGACCACATGAAGCAGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCACCCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAA,GCTCGCTTTCTTGCTGTCCAATTTCTATTAAAGGTTCCTTTGTTCCCTAAGTCCAACTACTAAACTGGGGGATATTATGAAGGGCCTTGAGCATCTGGAT,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,,Enhanced GFP,Aequorea victoria,HEK293T,52.4,Human beta-globin UTRs. Classic reporter construct for mRNA transfection.
|
| 3 |
+
2,mCherry-AlbUTR,ACATTTGCTTCTGACACAACTGTGTTCACTAGCAACCTCAAACAGACACCATGGTGCATCTGACTCCTGAGGAGAAGTCT,ATGGTGAGCAAGGGCGAGGAGGATAACATGGCCATCATCAAGGAGTTCATGCGCTTCAAGGTGCACATGGAGGGCTCCGTGAACGGCCACGAGTTCGAGATCGAGGGCGAGGGCGAGGGCCGCCCCTACGAGGGCACCCAGACCGCCAAGCTGAAGGTGACCAAGGGTGGCCCCCTGCCCTTCGCCTGGGACATCCTGTCCCCTCAGTTCATGTACGGCTCCAAGGCCTACGTGAAGCACCCCGCCGACATCCCCGACTACTTGAAGCTGTCCTTCCCCGAGGGCTTCAAGTGGGAGCGCGTGATGAACTTCGAGGACGGCGGCGTGGTGACCGTGACCCAGGACTCCTCCCTGCAGGACGGCGAGTTCATCTACAAGGTGAAGCTGCGCGGCACCAACTTCCCCTCCGACGGCCCCGTAATGCAGAAGAAGACCATGGGCTGGGAGGCCTCCTCCGAGCGGATGTACCCCGAGGACGGCGCCCTGAAGGGCGAGATCAAGCAGAGGCTGAAGCTGAAGGACGGCGGCCACTACGACGCTGAGGTCAAGACCACCTACAAGGCCAAGAAGCCCGTGCAGCTGCCCGGCGCCTACAACGTCAACATCAAGTTGGACATCACCTCCCACAACGAGGACTACACCATCGTGGAACAGTACGAACGCGCCGAGGGCCGCCACTCCACCGGCGGCATGGACGAGCTGTACAAGTAA,AATAAAGATCTTTATTTTCATTAGATCTGTGTGTTGGTTTTTTGTGTGAATCGATAGTACTAAATACTTTTCAGACACCAGAAATGCAGAGCAGTTCAGA,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,,mCherry red fluorescent protein,Discosoma sp.,CHO,50.1,Albumin 3'UTR for extended expression. Good cell viability.
|
demo/utr_library.csv
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gene_name,five_prime_utr,three_prime_utr,utr5_name,utr3_name,organism,notes
|
| 2 |
+
hBG-UTRs,ACATTTGCTTCTGACACAACTGTGTTCACTAGCAACCTCAAACAGACACCATGGTGCATCTGACTCCTGAGGAGAAGTCT,GCTCGCTTTCTTGCTGTCCAATTTCTATTAAAGGTTCCTTTGTTCCCTAAGTCCAACTACTAAACTGGGGGATATTATGAAGGGCCTTGAGCATCTGGAT,Human beta-globin 5'UTR,Human beta-globin 3'UTR,Homo sapiens,Classic mRNA therapeutic UTRs from BNT162b2. High translation efficiency in human cells.
|
| 3 |
+
Albumin-UTRs,AGATCTTCTTTTAAATTTCTTTTTACTGAATTCAGCCAATATATGTAATCCTACTTTCAATCAATTTTCCTAAGCAATG,AATAAAGATCTTTATTTTCATTAGATCTGTGTGTTGGTTTTTTGTGTGAATCGATAGTACTAAATACTTTTCAGACACCAGAAATGCAGAGCAGTTCAGA,Human albumin 5'UTR,Human albumin 3'UTR,Homo sapiens,Albumin UTRs for extended expression and mRNA stability.
|
| 4 |
+
Alpha-globin-UTRs,ACTCTTCTGGTCCCCACAGACTCAGAGAGAACCCACCATG,GCTGGAGCCTCGGTGGCCATGCTTCTTGCCCCTTGGGCCTCCCCCCAGCCCCTCCTCCCCTTCCTGCACCCGTACCCCCGTGGTCTTTGAATAAAGTCTGA,Human alpha-globin 5'UTR,Human alpha-globin 3'UTR,Homo sapiens,Alpha-globin UTRs. Commonly used in mRNA vaccine designs (Moderna mRNA-1273).
|
| 5 |
+
TEV-leader,AATTAAAATTTTATTTTTTTTTTTTGGAATATAAATG,,TEV 5' leader sequence,,Tobacco etch virus,TEV leader for cap-independent translation. Often used with viral IRES elements.
|
| 6 |
+
HCV-IRES,CCTGTGAGGAACTACTGTCTTCACGCAGAAAGCGTCTAGCCATGGCGTTAGTATGAGTGTCGTGCAGCCTCCAGGACCCCCCCTCCCGGGAGAGCCATAGTGGTCTGCGGAACCGGTGAGTACACCGGAATTGCCAGGACGACCGGGTCCTTTCTTGGATCAACCCGCTCAATGCCTGGAGATTTGGGCGTGCCCCCGCGAGACTGCTAGCCGAGTAGTGTTGGGTCGCGAAAGGCCTTGTGGTACTGCCTGATAGGGTGCTTGCGAGTGCCCCGGGAGGTCTCGTAGACCGTGCACCATG,,HCV IRES 5'UTR,,Hepatitis C virus,Full HCV IRES for cap-independent translation initiation.
|
| 7 |
+
Xenopus-bGlobin,GCTTGTTCTTTTTGCAGAAGCTCAGAATAAACGCTCAACTTTGGCAGATCG,TGTCACCATGGTCTTTCTTCTTTCTCTCCTCTTTTCTTTTTAATTAATAAAAAATGGAAAGAACCTCAGAAC,Xenopus beta-globin 5'UTR,Xenopus beta-globin 3'UTR,Xenopus laevis,Widely used in IVT mRNA research. Standard reference UTR pair.
|
models/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mRNA Scoring Models
|
| 2 |
+
|
| 3 |
+
This directory contains built-in mRNA scoring models for the mRNA Design Studio.
|
| 4 |
+
|
| 5 |
+
## Available Models
|
| 6 |
+
|
| 7 |
+
### 1. RNAstructure MFE Scorer (`rna_structure_scorer.py`)
|
| 8 |
+
|
| 9 |
+
**Purpose**: Predicts the minimum free energy (MFE) of mRNA secondary structure.
|
| 10 |
+
|
| 11 |
+
**Method**: Uses ViennaRNA RNAfold algorithm to compute the thermodynamic stability of RNA secondary structures. More negative MFE values indicate stronger secondary structure formation.
|
| 12 |
+
|
| 13 |
+
**Score Range**: 0-100
|
| 14 |
+
- **0-40**: Weak/unstable secondary structure (may be too unstructured)
|
| 15 |
+
- **40-70**: Moderate secondary structure (**optimal range** for translation)
|
| 16 |
+
- **70-100**: Strong secondary structure (may inhibit translation)
|
| 17 |
+
|
| 18 |
+
**Dependencies**:
|
| 19 |
+
- ViennaRNA Python package (optional)
|
| 20 |
+
- If ViennaRNA is not available, falls back to GC-content based proxy scoring
|
| 21 |
+
|
| 22 |
+
**Usage**:
|
| 23 |
+
```python
|
| 24 |
+
from models import RNAStructureMFEScorer
|
| 25 |
+
|
| 26 |
+
scorer = RNAStructureMFEScorer()
|
| 27 |
+
score = scorer.score(sequence)
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
**Interpretation**:
|
| 31 |
+
- Target moderate scores (40-70) for optimal translation efficiency
|
| 32 |
+
- Very low scores suggest the mRNA may be prone to degradation
|
| 33 |
+
- Very high scores suggest strong secondary structures that may block ribosome access
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
### 2. mRNA Stability Scorer (`mrna_stability_scorer.py`)
|
| 38 |
+
|
| 39 |
+
**Purpose**: Composite stability prediction based on multiple sequence features.
|
| 40 |
+
|
| 41 |
+
**Method**: Combines five established mRNA design principles:
|
| 42 |
+
1. **GC Content** (30% weight) - Optimal: 50-60%
|
| 43 |
+
2. **Codon Adaptation Index (CAI)** (25% weight) - Codon optimization for host organism
|
| 44 |
+
3. **Homopolymer Detection** (20% weight) - Penalizes long runs of identical nucleotides
|
| 45 |
+
4. **5' UTR Structure** (15% weight) - Moderate stability preferred
|
| 46 |
+
5. **Kozak Consensus** (10% weight) - Translation initiation efficiency
|
| 47 |
+
|
| 48 |
+
**Score Range**: 0-100
|
| 49 |
+
- **0-40**: Poor stability/translation efficiency
|
| 50 |
+
- **40-70**: Acceptable design
|
| 51 |
+
- **70-100**: Excellent design
|
| 52 |
+
|
| 53 |
+
**Dependencies**:
|
| 54 |
+
- BioPython (optional, for advanced CAI calculation)
|
| 55 |
+
- ViennaRNA (optional, for UTR structure analysis)
|
| 56 |
+
|
| 57 |
+
**Parameters**:
|
| 58 |
+
- `organism` (default: "human") - Target organism for codon optimization
|
| 59 |
+
|
| 60 |
+
**Usage**:
|
| 61 |
+
```python
|
| 62 |
+
from models import mRNAStabilityScorer
|
| 63 |
+
|
| 64 |
+
scorer = mRNAStabilityScorer(organism="human")
|
| 65 |
+
score = scorer.score(sequence)
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
**Individual Component Scores**:
|
| 69 |
+
|
| 70 |
+
You can access individual component scores for detailed analysis:
|
| 71 |
+
```python
|
| 72 |
+
scorer = mRNAStabilityScorer()
|
| 73 |
+
|
| 74 |
+
# Individual component scores
|
| 75 |
+
gc_score = scorer._score_gc_content(sequence) # 0-100
|
| 76 |
+
cai_score = scorer._score_cai(sequence) # 0-100
|
| 77 |
+
homopoly_score = scorer._score_homopolymers(sequence) # 0-100
|
| 78 |
+
utr_score = scorer._score_utr_structure(sequence) # 0-100
|
| 79 |
+
kozak_score = scorer._score_kozak(sequence) # 0-100
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
**Interpretation**:
|
| 83 |
+
- **70+**: Well-designed mRNA suitable for production
|
| 84 |
+
- **40-70**: Moderate quality, may benefit from optimization
|
| 85 |
+
- **<40**: Significant design issues, optimization strongly recommended
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
## Model Registry Integration
|
| 90 |
+
|
| 91 |
+
Both models implement the `ScoringModel` interface and can be loaded into the ModelRegistry:
|
| 92 |
+
|
| 93 |
+
```python
|
| 94 |
+
from models import ModelRegistry, RNAStructureMFEScorer, mRNAStabilityScorer
|
| 95 |
+
|
| 96 |
+
registry = ModelRegistry()
|
| 97 |
+
|
| 98 |
+
# Register built-in models
|
| 99 |
+
registry._register(RNAStructureMFEScorer(), "scoring", "builtin", "models/rna_structure_scorer.py")
|
| 100 |
+
registry._register(mRNAStabilityScorer(), "scoring", "builtin", "models/mrna_stability_scorer.py")
|
| 101 |
+
|
| 102 |
+
# Run scoring on sequences
|
| 103 |
+
import pandas as pd
|
| 104 |
+
results = registry.run_scoring("RNAstructure MFE", sequences)
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## Testing
|
| 110 |
+
|
| 111 |
+
Run tests for both models:
|
| 112 |
+
```bash
|
| 113 |
+
pytest tests/test_models.py::TestRNAStructureMFEScorer -v
|
| 114 |
+
pytest tests/test_models.py::TestmRNAStabilityScorer -v
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
---
|
| 118 |
+
|
| 119 |
+
## Adding Custom Models
|
| 120 |
+
|
| 121 |
+
To add your own scoring model:
|
| 122 |
+
|
| 123 |
+
1. Create a new Python file in this directory
|
| 124 |
+
2. Import and subclass `ScoringModel`:
|
| 125 |
+
|
| 126 |
+
```python
|
| 127 |
+
from models.base import ScoringModel
|
| 128 |
+
from core.models.sequence import mRNASequence
|
| 129 |
+
|
| 130 |
+
class MyCustomScorer(ScoringModel):
|
| 131 |
+
@property
|
| 132 |
+
def name(self) -> str:
|
| 133 |
+
return "My Custom Scorer"
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def description(self) -> str:
|
| 137 |
+
return "Description of what this model does"
|
| 138 |
+
|
| 139 |
+
def score(self, sequence: mRNASequence, metadata=None) -> float:
|
| 140 |
+
# Your scoring logic here
|
| 141 |
+
return 0.0 # Return score 0-100
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
3. Load it via the ModelRegistry:
|
| 145 |
+
|
| 146 |
+
```python
|
| 147 |
+
models = registry.load_local("path/to/your_model.py")
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
---
|
| 151 |
+
|
| 152 |
+
## References
|
| 153 |
+
|
| 154 |
+
### RNAstructure MFE Scorer
|
| 155 |
+
- Lorenz et al. (2011). "ViennaRNA Package 2.0." *Algorithms for Molecular Biology*, 6:26.
|
| 156 |
+
- Turner & Mathews (2010). "NNDB: the nearest neighbor parameter database for predicting stability of nucleic acid secondary structure." *Nucleic Acids Research*, 38:D280-282.
|
| 157 |
+
|
| 158 |
+
### mRNA Stability Scorer
|
| 159 |
+
- Mauro & Edelman (2002). "The ribosome filter hypothesis." *PNAS*, 99(19):12031-12036. (Kozak sequence)
|
| 160 |
+
- Sharp & Li (1987). "The codon adaptation indexβa measure of directional synonymous codon usage bias." *Nucleic Acids Research*, 15(3):1281-1295.
|
| 161 |
+
- Kudla et al. (2009). "Coding-sequence determinants of gene expression in Escherichia coli." *Science*, 324(5924):255-258.
|
| 162 |
+
- Presnyak et al. (2015). "Codon optimality is a major determinant of mRNA stability." *Cell*, 160(6):1111-1124.
|
models/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Model plugin system for mRNA Design Studio.
|
| 3 |
+
|
| 4 |
+
Included models:
|
| 5 |
+
- RNAStructureMFEScorer: Secondary structure MFE prediction
|
| 6 |
+
- mRNAStabilityScorer: Composite stability prediction based on multiple factors
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from models.base import (
|
| 10 |
+
ScoringModel,
|
| 11 |
+
GenerativeModel,
|
| 12 |
+
ModelRegistry,
|
| 13 |
+
APIScoringModel,
|
| 14 |
+
APIGenerativeModel,
|
| 15 |
+
RegisteredModel,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
from models.rna_structure_scorer import RNAStructureMFEScorer
|
| 19 |
+
from models.mrna_stability_scorer import mRNAStabilityScorer
|
| 20 |
+
|
| 21 |
+
__all__ = [
|
| 22 |
+
# Base classes
|
| 23 |
+
"ScoringModel",
|
| 24 |
+
"GenerativeModel",
|
| 25 |
+
"ModelRegistry",
|
| 26 |
+
"APIScoringModel",
|
| 27 |
+
"APIGenerativeModel",
|
| 28 |
+
"RegisteredModel",
|
| 29 |
+
# Concrete models
|
| 30 |
+
"RNAStructureMFEScorer",
|
| 31 |
+
"mRNAStabilityScorer",
|
| 32 |
+
]
|
models/base.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Model plugin system.
|
| 3 |
+
|
| 4 |
+
Users can contribute two types of models:
|
| 5 |
+
|
| 6 |
+
1. ScoringModel β scores an existing mRNASequence, returns a float.
|
| 7 |
+
2. GenerativeModel β generates new mRNASequences from constraints / seeds.
|
| 8 |
+
|
| 9 |
+
Models are loaded via ModelRegistry which supports:
|
| 10 |
+
- Local Python module (path on disk or importable package)
|
| 11 |
+
- Remote REST API endpoint (POST sequences β scores/generations)
|
| 12 |
+
|
| 13 |
+
The API adapter wraps HTTP calls behind the same interface so the UI
|
| 14 |
+
code never needs to know whether a model is local or remote.
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import importlib.util
|
| 19 |
+
import inspect
|
| 20 |
+
import sys
|
| 21 |
+
from abc import ABC, abstractmethod
|
| 22 |
+
from dataclasses import dataclass, field
|
| 23 |
+
from typing import Any, Dict, List, Optional, Type, Union
|
| 24 |
+
|
| 25 |
+
import pandas as pd
|
| 26 |
+
|
| 27 |
+
from core.models.sequence import mRNASequence
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ββ Abstract base classes ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
+
|
| 32 |
+
class ScoringModel(ABC):
|
| 33 |
+
"""
|
| 34 |
+
A model that assigns a numeric score to an mRNASequence.
|
| 35 |
+
|
| 36 |
+
Implement name and score(). scores_batch() has a default list
|
| 37 |
+
implementation but can be overridden for vectorised inference.
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
@property
|
| 41 |
+
@abstractmethod
|
| 42 |
+
def name(self) -> str:
|
| 43 |
+
"""Human-readable model name shown in the UI."""
|
| 44 |
+
...
|
| 45 |
+
|
| 46 |
+
@property
|
| 47 |
+
def description(self) -> str:
|
| 48 |
+
"""Optional description for the UI."""
|
| 49 |
+
return ""
|
| 50 |
+
|
| 51 |
+
@property
|
| 52 |
+
def version(self) -> str:
|
| 53 |
+
return "1.0"
|
| 54 |
+
|
| 55 |
+
@abstractmethod
|
| 56 |
+
def score(self, sequence: mRNASequence, metadata: Optional[Dict[str, Any]] = None) -> float:
|
| 57 |
+
"""
|
| 58 |
+
Score a single sequence.
|
| 59 |
+
|
| 60 |
+
Parameters
|
| 61 |
+
----------
|
| 62 |
+
sequence : mRNASequence
|
| 63 |
+
metadata : dict, optional
|
| 64 |
+
Raw database metadata attached to the sequence (raw_metadata).
|
| 65 |
+
|
| 66 |
+
Returns
|
| 67 |
+
-------
|
| 68 |
+
float
|
| 69 |
+
Score value. Convention: higher is better, but models may
|
| 70 |
+
define their own scale β document it in description.
|
| 71 |
+
"""
|
| 72 |
+
...
|
| 73 |
+
|
| 74 |
+
def score_batch(
|
| 75 |
+
self,
|
| 76 |
+
sequences: List[mRNASequence],
|
| 77 |
+
metadata: Optional[List[Optional[Dict[str, Any]]]] = None,
|
| 78 |
+
) -> List[float]:
|
| 79 |
+
"""Score a list of sequences. Override for vectorised models."""
|
| 80 |
+
metas = metadata or [None] * len(sequences)
|
| 81 |
+
return [self.score(seq, meta) for seq, meta in zip(sequences, metas)]
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class GenerativeModel(ABC):
|
| 85 |
+
"""
|
| 86 |
+
A model that generates new mRNASequences from constraints or seed sequences.
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
@property
|
| 90 |
+
@abstractmethod
|
| 91 |
+
def name(self) -> str:
|
| 92 |
+
...
|
| 93 |
+
|
| 94 |
+
@property
|
| 95 |
+
def description(self) -> str:
|
| 96 |
+
return ""
|
| 97 |
+
|
| 98 |
+
@property
|
| 99 |
+
def version(self) -> str:
|
| 100 |
+
return "1.0"
|
| 101 |
+
|
| 102 |
+
@abstractmethod
|
| 103 |
+
def generate(
|
| 104 |
+
self,
|
| 105 |
+
constraints: Dict[str, Any],
|
| 106 |
+
n: int = 10,
|
| 107 |
+
seed: Optional[mRNASequence] = None,
|
| 108 |
+
) -> List[mRNASequence]:
|
| 109 |
+
"""
|
| 110 |
+
Generate n sequences from the given constraints.
|
| 111 |
+
|
| 112 |
+
Parameters
|
| 113 |
+
----------
|
| 114 |
+
constraints : dict
|
| 115 |
+
Model-specific constraint dict (e.g. target GC, CAI, organism, etc.)
|
| 116 |
+
n : int
|
| 117 |
+
Number of sequences to generate.
|
| 118 |
+
seed : mRNASequence, optional
|
| 119 |
+
Seed sequence for mutation-based generators.
|
| 120 |
+
|
| 121 |
+
Returns
|
| 122 |
+
-------
|
| 123 |
+
List[mRNASequence]
|
| 124 |
+
"""
|
| 125 |
+
...
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
ModelType = Union[ScoringModel, GenerativeModel]
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# ββ API Adapter ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 132 |
+
|
| 133 |
+
class APIScoringModel(ScoringModel):
|
| 134 |
+
"""
|
| 135 |
+
Wraps a remote REST API behind the ScoringModel interface.
|
| 136 |
+
|
| 137 |
+
Expected API contract:
|
| 138 |
+
POST {endpoint}/score
|
| 139 |
+
Body: {"sequences": [{"id": ..., "sequence": ...}, ...]}
|
| 140 |
+
Response: {"scores": [{"id": ..., "score": float}, ...]}
|
| 141 |
+
"""
|
| 142 |
+
|
| 143 |
+
def __init__(
|
| 144 |
+
self,
|
| 145 |
+
endpoint: str,
|
| 146 |
+
model_name: str,
|
| 147 |
+
api_key: Optional[str] = None,
|
| 148 |
+
description: str = "",
|
| 149 |
+
version: str = "1.0",
|
| 150 |
+
timeout: float = 30.0,
|
| 151 |
+
) -> None:
|
| 152 |
+
self._endpoint = endpoint.rstrip("/")
|
| 153 |
+
self._name = model_name
|
| 154 |
+
self._api_key = api_key
|
| 155 |
+
self._description = description
|
| 156 |
+
self._version = version
|
| 157 |
+
self._timeout = timeout
|
| 158 |
+
|
| 159 |
+
@property
|
| 160 |
+
def name(self) -> str:
|
| 161 |
+
return self._name
|
| 162 |
+
|
| 163 |
+
@property
|
| 164 |
+
def description(self) -> str:
|
| 165 |
+
return self._description
|
| 166 |
+
|
| 167 |
+
@property
|
| 168 |
+
def version(self) -> str:
|
| 169 |
+
return self._version
|
| 170 |
+
|
| 171 |
+
def _headers(self) -> Dict[str, str]:
|
| 172 |
+
h = {"Content-Type": "application/json"}
|
| 173 |
+
if self._api_key:
|
| 174 |
+
h["Authorization"] = f"Bearer {self._api_key}"
|
| 175 |
+
return h
|
| 176 |
+
|
| 177 |
+
def score(self, sequence: mRNASequence, metadata: Optional[Dict[str, Any]] = None) -> float:
|
| 178 |
+
results = self.score_batch([sequence], [metadata])
|
| 179 |
+
return results[0]
|
| 180 |
+
|
| 181 |
+
def score_batch(
|
| 182 |
+
self,
|
| 183 |
+
sequences: List[mRNASequence],
|
| 184 |
+
metadata: Optional[List[Optional[Dict[str, Any]]]] = None,
|
| 185 |
+
) -> List[float]:
|
| 186 |
+
import httpx
|
| 187 |
+
|
| 188 |
+
payload = {
|
| 189 |
+
"sequences": [
|
| 190 |
+
{
|
| 191 |
+
"id": seq.id,
|
| 192 |
+
"name": seq.name,
|
| 193 |
+
"sequence": seq.assembled_sequence,
|
| 194 |
+
"metadata": (metadata[i] if metadata else None),
|
| 195 |
+
}
|
| 196 |
+
for i, seq in enumerate(sequences)
|
| 197 |
+
]
|
| 198 |
+
}
|
| 199 |
+
response = httpx.post(
|
| 200 |
+
f"{self._endpoint}/score",
|
| 201 |
+
json=payload,
|
| 202 |
+
headers=self._headers(),
|
| 203 |
+
timeout=self._timeout,
|
| 204 |
+
)
|
| 205 |
+
response.raise_for_status()
|
| 206 |
+
data = response.json()
|
| 207 |
+
score_map = {item["id"]: item["score"] for item in data["scores"]}
|
| 208 |
+
return [score_map.get(seq.id, float("nan")) for seq in sequences]
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
class APIGenerativeModel(GenerativeModel):
|
| 212 |
+
"""
|
| 213 |
+
Wraps a remote REST API behind the GenerativeModel interface.
|
| 214 |
+
|
| 215 |
+
Expected API contract:
|
| 216 |
+
POST {endpoint}/generate
|
| 217 |
+
Body: {"constraints": {...}, "n": int, "seed_sequence": str | null}
|
| 218 |
+
Response: {"sequences": [{"name": ..., "cds": ..., ...}, ...]}
|
| 219 |
+
"""
|
| 220 |
+
|
| 221 |
+
def __init__(
|
| 222 |
+
self,
|
| 223 |
+
endpoint: str,
|
| 224 |
+
model_name: str,
|
| 225 |
+
api_key: Optional[str] = None,
|
| 226 |
+
description: str = "",
|
| 227 |
+
version: str = "1.0",
|
| 228 |
+
timeout: float = 60.0,
|
| 229 |
+
) -> None:
|
| 230 |
+
self._endpoint = endpoint.rstrip("/")
|
| 231 |
+
self._name = model_name
|
| 232 |
+
self._api_key = api_key
|
| 233 |
+
self._description = description
|
| 234 |
+
self._version = version
|
| 235 |
+
self._timeout = timeout
|
| 236 |
+
|
| 237 |
+
@property
|
| 238 |
+
def name(self) -> str:
|
| 239 |
+
return self._name
|
| 240 |
+
|
| 241 |
+
@property
|
| 242 |
+
def description(self) -> str:
|
| 243 |
+
return self._description
|
| 244 |
+
|
| 245 |
+
@property
|
| 246 |
+
def version(self) -> str:
|
| 247 |
+
return self._version
|
| 248 |
+
|
| 249 |
+
def _headers(self) -> Dict[str, str]:
|
| 250 |
+
h = {"Content-Type": "application/json"}
|
| 251 |
+
if self._api_key:
|
| 252 |
+
h["Authorization"] = f"Bearer {self._api_key}"
|
| 253 |
+
return h
|
| 254 |
+
|
| 255 |
+
def generate(
|
| 256 |
+
self,
|
| 257 |
+
constraints: Dict[str, Any],
|
| 258 |
+
n: int = 10,
|
| 259 |
+
seed: Optional[mRNASequence] = None,
|
| 260 |
+
) -> List[mRNASequence]:
|
| 261 |
+
import httpx
|
| 262 |
+
|
| 263 |
+
payload = {
|
| 264 |
+
"constraints": constraints,
|
| 265 |
+
"n": n,
|
| 266 |
+
"seed_sequence": seed.assembled_sequence if seed else None,
|
| 267 |
+
}
|
| 268 |
+
response = httpx.post(
|
| 269 |
+
f"{self._endpoint}/generate",
|
| 270 |
+
json=payload,
|
| 271 |
+
headers=self._headers(),
|
| 272 |
+
timeout=self._timeout,
|
| 273 |
+
)
|
| 274 |
+
response.raise_for_status()
|
| 275 |
+
data = response.json()
|
| 276 |
+
return [mRNASequence.from_dict({**item, "source": "local"}) for item in data["sequences"]]
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# ββ Model Registry βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 280 |
+
|
| 281 |
+
@dataclass
|
| 282 |
+
class RegisteredModel:
|
| 283 |
+
model: ModelType
|
| 284 |
+
model_type: str # "scoring" | "generative"
|
| 285 |
+
source: str # "local" | "api" | "builtin" | "catalog"
|
| 286 |
+
source_path: str = "" # file path or endpoint URL
|
| 287 |
+
repository: str = "" # display provenance (e.g. "github.com/ViennaRNA")
|
| 288 |
+
category: str = "" # model category for display
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
class ModelRegistry:
|
| 292 |
+
"""
|
| 293 |
+
Manages loaded scoring and generative models.
|
| 294 |
+
|
| 295 |
+
Models are registered either by loading a local Python file/module
|
| 296 |
+
or by configuring an API endpoint.
|
| 297 |
+
"""
|
| 298 |
+
|
| 299 |
+
def __init__(self) -> None:
|
| 300 |
+
self._models: Dict[str, RegisteredModel] = {}
|
| 301 |
+
|
| 302 |
+
# ββ Loading ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 303 |
+
|
| 304 |
+
def load_local(self, path: str) -> List[ModelType]:
|
| 305 |
+
"""
|
| 306 |
+
Dynamically import a Python file and register all ScoringModel /
|
| 307 |
+
GenerativeModel subclasses found in it.
|
| 308 |
+
|
| 309 |
+
Returns the list of loaded model instances.
|
| 310 |
+
"""
|
| 311 |
+
spec = importlib.util.spec_from_file_location("_user_model", path)
|
| 312 |
+
if spec is None or spec.loader is None:
|
| 313 |
+
raise ImportError(f"Cannot load module from: {path}")
|
| 314 |
+
module = importlib.util.module_from_spec(spec)
|
| 315 |
+
spec.loader.exec_module(module) # type: ignore[union-attr]
|
| 316 |
+
|
| 317 |
+
loaded: List[ModelType] = []
|
| 318 |
+
for _, obj in inspect.getmembers(module, inspect.isclass):
|
| 319 |
+
if obj.__module__ != module.__name__:
|
| 320 |
+
continue
|
| 321 |
+
if issubclass(obj, ScoringModel) and obj is not ScoringModel:
|
| 322 |
+
instance = obj()
|
| 323 |
+
self._register(instance, "scoring", "local", path)
|
| 324 |
+
loaded.append(instance)
|
| 325 |
+
elif issubclass(obj, GenerativeModel) and obj is not GenerativeModel:
|
| 326 |
+
instance = obj()
|
| 327 |
+
self._register(instance, "generative", "local", path)
|
| 328 |
+
loaded.append(instance)
|
| 329 |
+
|
| 330 |
+
if not loaded:
|
| 331 |
+
raise ValueError(
|
| 332 |
+
f"No ScoringModel or GenerativeModel subclasses found in {path}."
|
| 333 |
+
)
|
| 334 |
+
return loaded
|
| 335 |
+
|
| 336 |
+
def register_api_scorer(
|
| 337 |
+
self,
|
| 338 |
+
endpoint: str,
|
| 339 |
+
model_name: str,
|
| 340 |
+
api_key: Optional[str] = None,
|
| 341 |
+
description: str = "",
|
| 342 |
+
) -> APIScoringModel:
|
| 343 |
+
"""Register a remote scoring API."""
|
| 344 |
+
model = APIScoringModel(
|
| 345 |
+
endpoint=endpoint,
|
| 346 |
+
model_name=model_name,
|
| 347 |
+
api_key=api_key,
|
| 348 |
+
description=description,
|
| 349 |
+
)
|
| 350 |
+
self._register(model, "scoring", "api", endpoint)
|
| 351 |
+
return model
|
| 352 |
+
|
| 353 |
+
def register_api_generator(
|
| 354 |
+
self,
|
| 355 |
+
endpoint: str,
|
| 356 |
+
model_name: str,
|
| 357 |
+
api_key: Optional[str] = None,
|
| 358 |
+
description: str = "",
|
| 359 |
+
) -> APIGenerativeModel:
|
| 360 |
+
"""Register a remote generative API."""
|
| 361 |
+
model = APIGenerativeModel(
|
| 362 |
+
endpoint=endpoint,
|
| 363 |
+
model_name=model_name,
|
| 364 |
+
api_key=api_key,
|
| 365 |
+
description=description,
|
| 366 |
+
)
|
| 367 |
+
self._register(model, "generative", "api", endpoint)
|
| 368 |
+
return model
|
| 369 |
+
|
| 370 |
+
# ββ Running ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 371 |
+
|
| 372 |
+
def run_scoring(
|
| 373 |
+
self,
|
| 374 |
+
model_name: str,
|
| 375 |
+
sequences: List[mRNASequence],
|
| 376 |
+
) -> pd.DataFrame:
|
| 377 |
+
"""
|
| 378 |
+
Run a scoring model against sequences and return a DataFrame.
|
| 379 |
+
|
| 380 |
+
Columns: id, name, score
|
| 381 |
+
"""
|
| 382 |
+
reg = self._get(model_name, "scoring")
|
| 383 |
+
scorer: ScoringModel = reg.model # type: ignore[assignment]
|
| 384 |
+
scores = scorer.score_batch(sequences)
|
| 385 |
+
return pd.DataFrame({
|
| 386 |
+
"id": [s.id for s in sequences],
|
| 387 |
+
"name": [s.name for s in sequences],
|
| 388 |
+
"score": scores,
|
| 389 |
+
})
|
| 390 |
+
|
| 391 |
+
def run_generation(
|
| 392 |
+
self,
|
| 393 |
+
model_name: str,
|
| 394 |
+
constraints: Dict[str, Any],
|
| 395 |
+
n: int = 10,
|
| 396 |
+
seed: Optional[mRNASequence] = None,
|
| 397 |
+
) -> List[mRNASequence]:
|
| 398 |
+
"""Run a generative model and return new sequences."""
|
| 399 |
+
reg = self._get(model_name, "generative")
|
| 400 |
+
generator: GenerativeModel = reg.model # type: ignore[assignment]
|
| 401 |
+
return generator.generate(constraints, n=n, seed=seed)
|
| 402 |
+
|
| 403 |
+
# ββ Query ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 404 |
+
|
| 405 |
+
@property
|
| 406 |
+
def scoring_models(self) -> List[RegisteredModel]:
|
| 407 |
+
return [r for r in self._models.values() if r.model_type == "scoring"]
|
| 408 |
+
|
| 409 |
+
@property
|
| 410 |
+
def generative_models(self) -> List[RegisteredModel]:
|
| 411 |
+
return [r for r in self._models.values() if r.model_type == "generative"]
|
| 412 |
+
|
| 413 |
+
@property
|
| 414 |
+
def all_models(self) -> List[RegisteredModel]:
|
| 415 |
+
return list(self._models.values())
|
| 416 |
+
|
| 417 |
+
def unregister(self, model_name: str) -> bool:
|
| 418 |
+
if model_name in self._models:
|
| 419 |
+
del self._models[model_name]
|
| 420 |
+
return True
|
| 421 |
+
return False
|
| 422 |
+
|
| 423 |
+
# ββ Internal βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 424 |
+
|
| 425 |
+
def _register(
|
| 426 |
+
self,
|
| 427 |
+
model: ModelType,
|
| 428 |
+
model_type: str,
|
| 429 |
+
source: str,
|
| 430 |
+
source_path: str,
|
| 431 |
+
) -> None:
|
| 432 |
+
self._models[model.name] = RegisteredModel(
|
| 433 |
+
model=model,
|
| 434 |
+
model_type=model_type,
|
| 435 |
+
source=source,
|
| 436 |
+
source_path=source_path,
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
def _get(self, name: str, expected_type: str) -> RegisteredModel:
|
| 440 |
+
if name not in self._models:
|
| 441 |
+
raise KeyError(f"Model '{name}' not found in registry.")
|
| 442 |
+
reg = self._models[name]
|
| 443 |
+
if reg.model_type != expected_type:
|
| 444 |
+
raise TypeError(
|
| 445 |
+
f"Model '{name}' is a {reg.model_type} model, not {expected_type}."
|
| 446 |
+
)
|
| 447 |
+
return reg
|
models/catalog.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Curated catalog of public mRNA models for the Model Repository browser.
|
| 3 |
+
|
| 4 |
+
Each entry represents a real publicly available model/tool. The catalog
|
| 5 |
+
is hardcoded for demo purposes β in production this would be fetched
|
| 6 |
+
from a model registry API.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from typing import List
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class ModelCatalogEntry:
|
| 16 |
+
"""A model available for import from the catalog."""
|
| 17 |
+
name: str
|
| 18 |
+
model_type: str # "analytical" | "generative"
|
| 19 |
+
category: str # "Structure", "Stability", "Codon Optimization", etc.
|
| 20 |
+
description: str
|
| 21 |
+
repository: str # e.g. "github.com/ViennaRNA/ViennaRNA"
|
| 22 |
+
repository_url: str
|
| 23 |
+
deployment: str # "embedded" | "api" | "both"
|
| 24 |
+
version: str
|
| 25 |
+
paper: str # citation
|
| 26 |
+
paper_url: str
|
| 27 |
+
icon: str # emoji
|
| 28 |
+
tags: List[str] = field(default_factory=list)
|
| 29 |
+
inputs: str = "" # "Full mRNA sequence", "CDS only", etc.
|
| 30 |
+
status: str = "available" # "available" | "imported" | "connected"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def get_model_catalog() -> List[ModelCatalogEntry]:
|
| 34 |
+
"""Return the curated model catalog."""
|
| 35 |
+
catalog: List[ModelCatalogEntry] = []
|
| 36 |
+
|
| 37 |
+
# ββ Analytical Models ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
+
|
| 39 |
+
catalog.append(ModelCatalogEntry(
|
| 40 |
+
name="ViennaRNA (RNAfold)",
|
| 41 |
+
model_type="analytical",
|
| 42 |
+
category="Structure Prediction",
|
| 43 |
+
description="Gold-standard thermodynamic RNA secondary structure prediction. Computes MFE structures, partition functions, and base-pair probabilities.",
|
| 44 |
+
repository="github.com/ViennaRNA/ViennaRNA",
|
| 45 |
+
repository_url="https://github.com/ViennaRNA/ViennaRNA",
|
| 46 |
+
deployment="embedded",
|
| 47 |
+
version="2.6.4",
|
| 48 |
+
paper="Lorenz et al., ViennaRNA Package 2.0, Algorithms Mol Biol (2011)",
|
| 49 |
+
paper_url="https://doi.org/10.1186/1748-7188-6-26",
|
| 50 |
+
icon="π§¬",
|
| 51 |
+
tags=["scoring", "MFE", "secondary structure", "thermodynamics"],
|
| 52 |
+
inputs="Full mRNA sequence",
|
| 53 |
+
))
|
| 54 |
+
|
| 55 |
+
catalog.append(ModelCatalogEntry(
|
| 56 |
+
name="LinearFold",
|
| 57 |
+
model_type="analytical",
|
| 58 |
+
category="Structure Prediction",
|
| 59 |
+
description="Linear-time RNA secondary structure prediction using beam search. Orders of magnitude faster than cubic-time algorithms on long sequences.",
|
| 60 |
+
repository="github.com/LinearFold/LinearFold",
|
| 61 |
+
repository_url="https://github.com/LinearFold/LinearFold",
|
| 62 |
+
deployment="embedded",
|
| 63 |
+
version="1.0",
|
| 64 |
+
paper="Huang et al., LinearFold: linear-time approximate RNA folding, Bioinformatics (2019)",
|
| 65 |
+
paper_url="https://doi.org/10.1093/bioinformatics/btz375",
|
| 66 |
+
icon="β‘",
|
| 67 |
+
tags=["scoring", "MFE", "secondary structure", "fast"],
|
| 68 |
+
inputs="Full mRNA sequence",
|
| 69 |
+
))
|
| 70 |
+
|
| 71 |
+
catalog.append(ModelCatalogEntry(
|
| 72 |
+
name="EternaFold",
|
| 73 |
+
model_type="analytical",
|
| 74 |
+
category="Structure Prediction",
|
| 75 |
+
description="RNA secondary structure prediction trained on Eterna player data. Improved accuracy on structured RNA elements.",
|
| 76 |
+
repository="github.com/eternagame/EternaFold",
|
| 77 |
+
repository_url="https://github.com/eternagame/EternaFold",
|
| 78 |
+
deployment="embedded",
|
| 79 |
+
version="1.3",
|
| 80 |
+
paper="Wayment-Steele et al., RNA secondary structure packages evaluated, Nat Methods (2022)",
|
| 81 |
+
paper_url="https://doi.org/10.1038/s41592-022-01605-0",
|
| 82 |
+
icon="π―",
|
| 83 |
+
tags=["scoring", "secondary structure", "deep learning"],
|
| 84 |
+
inputs="Full mRNA sequence",
|
| 85 |
+
))
|
| 86 |
+
|
| 87 |
+
catalog.append(ModelCatalogEntry(
|
| 88 |
+
name="Optimus 5-Prime",
|
| 89 |
+
model_type="analytical",
|
| 90 |
+
category="UTR Scoring",
|
| 91 |
+
description="Predicts mean ribosome load from 5' UTR sequence. Convolutional model trained on massively parallel reporter assays.",
|
| 92 |
+
repository="github.com/pjsample/human_5utr_modeling",
|
| 93 |
+
repository_url="https://github.com/pjsample/human_5utr_modeling",
|
| 94 |
+
deployment="embedded",
|
| 95 |
+
version="1.0",
|
| 96 |
+
paper="Sample et al., Human 5' UTR design and variant effect prediction, Nat Biotechnol (2019)",
|
| 97 |
+
paper_url="https://doi.org/10.1038/s41587-019-0164-5",
|
| 98 |
+
icon="π",
|
| 99 |
+
tags=["scoring", "UTR", "translation", "ribosome load"],
|
| 100 |
+
inputs="5' UTR only",
|
| 101 |
+
))
|
| 102 |
+
|
| 103 |
+
catalog.append(ModelCatalogEntry(
|
| 104 |
+
name="RNAdegformer",
|
| 105 |
+
model_type="analytical",
|
| 106 |
+
category="Stability / Degradation",
|
| 107 |
+
description="Transformer model predicting per-nucleotide RNA degradation rates. Trained on OpenVaccine challenge data.",
|
| 108 |
+
repository="github.com/Shujun-He/RNAdegformer",
|
| 109 |
+
repository_url="https://github.com/Shujun-He/RNAdegformer",
|
| 110 |
+
deployment="embedded",
|
| 111 |
+
version="1.0",
|
| 112 |
+
paper="He et al., RNAdegformer, competition solution (2020)",
|
| 113 |
+
paper_url="https://arxiv.org/abs/2110.07531",
|
| 114 |
+
icon="π",
|
| 115 |
+
tags=["scoring", "stability", "degradation", "transformer"],
|
| 116 |
+
inputs="Full mRNA sequence",
|
| 117 |
+
))
|
| 118 |
+
|
| 119 |
+
catalog.append(ModelCatalogEntry(
|
| 120 |
+
name="Saluki",
|
| 121 |
+
model_type="analytical",
|
| 122 |
+
category="Stability / Half-life",
|
| 123 |
+
description="Deep learning model predicting mRNA half-life from sequence and structure. Integrates codon usage, UTR features, and secondary structure.",
|
| 124 |
+
repository="github.com/vagarwal87/saluki_paper",
|
| 125 |
+
repository_url="https://github.com/vagarwal87/saluki_paper",
|
| 126 |
+
deployment="embedded",
|
| 127 |
+
version="1.0",
|
| 128 |
+
paper="Agarwal & Kelley, The genetic and biochemical determinants of mRNA degradation rates, Genome Biol (2022)",
|
| 129 |
+
paper_url="https://doi.org/10.1186/s13059-022-02811-x",
|
| 130 |
+
icon="β±οΈ",
|
| 131 |
+
tags=["scoring", "half-life", "stability", "deep learning"],
|
| 132 |
+
inputs="Full mRNA sequence",
|
| 133 |
+
))
|
| 134 |
+
|
| 135 |
+
catalog.append(ModelCatalogEntry(
|
| 136 |
+
name="CodonFM (NVIDIA)",
|
| 137 |
+
model_type="analytical",
|
| 138 |
+
category="Foundation Model",
|
| 139 |
+
description="Foundation model for codon-level mRNA representations. Pre-trained on millions of coding sequences for downstream tasks.",
|
| 140 |
+
repository="github.com/NVIDIA-Digital-Bio/CodonFM",
|
| 141 |
+
repository_url="https://github.com/NVIDIA-Digital-Bio/CodonFM",
|
| 142 |
+
deployment="both",
|
| 143 |
+
version="1.0",
|
| 144 |
+
paper="NVIDIA Digital Biology, CodonFM (2024)",
|
| 145 |
+
paper_url="https://github.com/NVIDIA-Digital-Bio/CodonFM",
|
| 146 |
+
icon="ποΈ",
|
| 147 |
+
tags=["foundation model", "embeddings", "codon", "NVIDIA"],
|
| 148 |
+
inputs="CDS only",
|
| 149 |
+
))
|
| 150 |
+
|
| 151 |
+
catalog.append(ModelCatalogEntry(
|
| 152 |
+
name="mRNABERT",
|
| 153 |
+
model_type="analytical",
|
| 154 |
+
category="Foundation Model",
|
| 155 |
+
description="BERT-based foundation model for mRNA sequences. Generates contextual embeddings useful for property prediction and design.",
|
| 156 |
+
repository="huggingface.co/YYLY66/mRNABERT",
|
| 157 |
+
repository_url="https://huggingface.co/YYLY66/mRNABERT",
|
| 158 |
+
deployment="embedded",
|
| 159 |
+
version="1.0",
|
| 160 |
+
paper="Yang et al., mRNABERT (2023)",
|
| 161 |
+
paper_url="https://huggingface.co/YYLY66/mRNABERT",
|
| 162 |
+
icon="π€",
|
| 163 |
+
tags=["foundation model", "BERT", "embeddings", "HuggingFace"],
|
| 164 |
+
inputs="Full mRNA sequence",
|
| 165 |
+
))
|
| 166 |
+
|
| 167 |
+
catalog.append(ModelCatalogEntry(
|
| 168 |
+
name="Riboformer",
|
| 169 |
+
model_type="analytical",
|
| 170 |
+
category="Translation Efficiency",
|
| 171 |
+
description="Transformer model predicting translation efficiency from mRNA sequence. Models ribosome dynamics and codon-level features.",
|
| 172 |
+
repository="Paper",
|
| 173 |
+
repository_url="https://doi.org/10.1101/2023.09.09.556981",
|
| 174 |
+
deployment="embedded",
|
| 175 |
+
version="1.0",
|
| 176 |
+
paper="Gu et al., Riboformer: a deep learning framework for predicting context-dependent translation dynamics (2023)",
|
| 177 |
+
paper_url="https://doi.org/10.1101/2023.09.09.556981",
|
| 178 |
+
icon="π¬",
|
| 179 |
+
tags=["scoring", "translation", "ribosome", "transformer"],
|
| 180 |
+
inputs="Full mRNA sequence",
|
| 181 |
+
))
|
| 182 |
+
|
| 183 |
+
# ββ Generative Models ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 184 |
+
|
| 185 |
+
catalog.append(ModelCatalogEntry(
|
| 186 |
+
name="GEMORNA",
|
| 187 |
+
model_type="generative",
|
| 188 |
+
category="Full mRNA Design",
|
| 189 |
+
description="Generative model for complete mRNA sequence design. Jointly optimizes codon usage, UTR selection, and structural stability.",
|
| 190 |
+
repository="github.com/RainaBio/GEMORNA",
|
| 191 |
+
repository_url="https://github.com/RainaBio/GEMORNA",
|
| 192 |
+
deployment="embedded",
|
| 193 |
+
version="1.0",
|
| 194 |
+
paper="Raina Bio, GEMORNA (2024)",
|
| 195 |
+
paper_url="https://github.com/RainaBio/GEMORNA",
|
| 196 |
+
icon="π§ͺ",
|
| 197 |
+
tags=["generative", "full mRNA", "end-to-end", "design"],
|
| 198 |
+
inputs="Target protein / constraints",
|
| 199 |
+
))
|
| 200 |
+
|
| 201 |
+
catalog.append(ModelCatalogEntry(
|
| 202 |
+
name="LinearDesign",
|
| 203 |
+
model_type="generative",
|
| 204 |
+
category="CDS Optimization",
|
| 205 |
+
description="Simultaneously optimizes mRNA sequence for codon usage and minimum free energy structure. Uses dynamic programming for global optimality.",
|
| 206 |
+
repository="github.com/LinearDesignSoftware/LinearDesign",
|
| 207 |
+
repository_url="https://github.com/LinearDesignSoftware/LinearDesign",
|
| 208 |
+
deployment="embedded",
|
| 209 |
+
version="1.0",
|
| 210 |
+
paper="Zhang et al., Algorithm for optimized mRNA design improves stability and immunogenicity, Nature (2023)",
|
| 211 |
+
paper_url="https://doi.org/10.1038/s41586-023-06127-z",
|
| 212 |
+
icon="π",
|
| 213 |
+
tags=["generative", "CDS", "codon optimization", "structure"],
|
| 214 |
+
inputs="CDS only",
|
| 215 |
+
))
|
| 216 |
+
|
| 217 |
+
catalog.append(ModelCatalogEntry(
|
| 218 |
+
name="mRNAid (Merck)",
|
| 219 |
+
model_type="generative",
|
| 220 |
+
category="mRNA Optimization",
|
| 221 |
+
description="Multi-objective mRNA optimization tool. Simultaneously optimizes GC content, codon usage, MFE, and uridine depletion.",
|
| 222 |
+
repository="github.com/Merck/mRNAid",
|
| 223 |
+
repository_url="https://github.com/Merck/mRNAid",
|
| 224 |
+
deployment="both",
|
| 225 |
+
version="1.0",
|
| 226 |
+
paper="Medina-Inojosa et al., mRNAid (2024)",
|
| 227 |
+
paper_url="https://github.com/Merck/mRNAid",
|
| 228 |
+
icon="π",
|
| 229 |
+
tags=["generative", "optimization", "multi-objective", "Merck"],
|
| 230 |
+
inputs="CDS only",
|
| 231 |
+
))
|
| 232 |
+
|
| 233 |
+
catalog.append(ModelCatalogEntry(
|
| 234 |
+
name="CodonTransformer",
|
| 235 |
+
model_type="generative",
|
| 236 |
+
category="Codon Optimization",
|
| 237 |
+
description="Transformer-based codon optimizer supporting 164 organisms. Generates optimized CDS from protein sequences using organism-specific codon preferences.",
|
| 238 |
+
repository="huggingface.co/Adibvafa/CodonTransformer",
|
| 239 |
+
repository_url="https://huggingface.co/Adibvafa/CodonTransformer",
|
| 240 |
+
deployment="embedded",
|
| 241 |
+
version="1.5",
|
| 242 |
+
paper="Farhadi et al., CodonTransformer (2024)",
|
| 243 |
+
paper_url="https://huggingface.co/Adibvafa/CodonTransformer",
|
| 244 |
+
icon="π",
|
| 245 |
+
tags=["generative", "codon optimization", "transformer", "multi-species"],
|
| 246 |
+
inputs="Protein sequence",
|
| 247 |
+
))
|
| 248 |
+
|
| 249 |
+
catalog.append(ModelCatalogEntry(
|
| 250 |
+
name="UTRGAN",
|
| 251 |
+
model_type="generative",
|
| 252 |
+
category="UTR Generation",
|
| 253 |
+
description="Generative adversarial network for designing functional 5' UTR sequences. Generates UTRs with target expression levels.",
|
| 254 |
+
repository="github.com/ciceklab/UTRGAN",
|
| 255 |
+
repository_url="https://github.com/ciceklab/UTRGAN",
|
| 256 |
+
deployment="embedded",
|
| 257 |
+
version="1.0",
|
| 258 |
+
paper="Daskalakis et al., UTRGAN, NeurIPS Workshop (2022)",
|
| 259 |
+
paper_url="https://github.com/ciceklab/UTRGAN",
|
| 260 |
+
icon="π²",
|
| 261 |
+
tags=["generative", "UTR", "GAN", "expression"],
|
| 262 |
+
inputs="Target expression level",
|
| 263 |
+
))
|
| 264 |
+
|
| 265 |
+
catalog.append(ModelCatalogEntry(
|
| 266 |
+
name="iCodon",
|
| 267 |
+
model_type="generative",
|
| 268 |
+
category="Codon Stability Optimization",
|
| 269 |
+
description="Codon optimization tool focusing on mRNA stability. Uses codon influence on mRNA stability to guide synonymous substitutions.",
|
| 270 |
+
repository="github.com/santiago1234/iCodon",
|
| 271 |
+
repository_url="https://github.com/santiago1234/iCodon",
|
| 272 |
+
deployment="embedded",
|
| 273 |
+
version="1.0",
|
| 274 |
+
paper="Diez et al., iCodon customizes gene expression based on codon influence on mRNA stability, Genome Biol (2022)",
|
| 275 |
+
paper_url="https://doi.org/10.1186/s13059-021-02571-y",
|
| 276 |
+
icon="π§",
|
| 277 |
+
tags=["generative", "codon optimization", "stability", "half-life"],
|
| 278 |
+
inputs="CDS only",
|
| 279 |
+
))
|
| 280 |
+
|
| 281 |
+
return catalog
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
# Category constants for filtering
|
| 285 |
+
ANALYTICAL_CATEGORIES = [
|
| 286 |
+
"Structure Prediction",
|
| 287 |
+
"UTR Scoring",
|
| 288 |
+
"Stability / Degradation",
|
| 289 |
+
"Stability / Half-life",
|
| 290 |
+
"Foundation Model",
|
| 291 |
+
"Translation Efficiency",
|
| 292 |
+
]
|
| 293 |
+
|
| 294 |
+
GENERATIVE_CATEGORIES = [
|
| 295 |
+
"Full mRNA Design",
|
| 296 |
+
"CDS Optimization",
|
| 297 |
+
"mRNA Optimization",
|
| 298 |
+
"Codon Optimization",
|
| 299 |
+
"UTR Generation",
|
| 300 |
+
"Codon Stability Optimization",
|
| 301 |
+
]
|
| 302 |
+
|
| 303 |
+
ALL_CATEGORIES = sorted(set(ANALYTICAL_CATEGORIES + GENERATIVE_CATEGORIES))
|
| 304 |
+
|
| 305 |
+
FILTER_OPTIONS = [
|
| 306 |
+
"All",
|
| 307 |
+
"Analytical",
|
| 308 |
+
"Generative",
|
| 309 |
+
"Structure",
|
| 310 |
+
"Stability",
|
| 311 |
+
"Codon",
|
| 312 |
+
"UTR",
|
| 313 |
+
"Foundation",
|
| 314 |
+
]
|