offtargeteffect commited on
Commit
99f834c
Β·
verified Β·
1 Parent(s): 3df7260

Deploy mRNA Design Studio (Docker SDK)

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .dockerignore +13 -0
  2. .env.example +22 -0
  3. .gitignore +55 -0
  4. Dockerfile +42 -0
  5. MODELS_ADDED.md +177 -0
  6. Makefile +155 -0
  7. Procfile +1 -0
  8. README.md +54 -5
  9. core/__init__.py +0 -0
  10. core/analysis/__init__.py +0 -0
  11. core/analysis/analyzer.py +276 -0
  12. core/analysis/cai.py +187 -0
  13. core/analysis/dinucleotide.py +74 -0
  14. core/analysis/gc_content.py +68 -0
  15. core/analysis/homopolymers.py +84 -0
  16. core/analysis/kozak.py +138 -0
  17. core/analysis/restriction_sites.py +174 -0
  18. core/analysis/structure.py +83 -0
  19. core/analysis/uridine.py +81 -0
  20. core/data/__init__.py +1 -0
  21. core/data/seed_backbones.py +80 -0
  22. core/data/seed_parts.py +197 -0
  23. core/database/__init__.py +42 -0
  24. core/database/base.py +204 -0
  25. core/database/csv_importer.py +97 -0
  26. core/database/postgres.py +81 -0
  27. core/database/sqlite.py +68 -0
  28. core/models/__init__.py +0 -0
  29. core/models/parts.py +173 -0
  30. core/models/plasmid.py +178 -0
  31. core/models/sequence.py +196 -0
  32. core/models/worklist.py +159 -0
  33. core/optimization/__init__.py +0 -0
  34. core/sequence_tools/__init__.py +0 -0
  35. core/sequence_tools/clean_for_cloning.py +172 -0
  36. core/sequence_tools/codon_optimizer.py +155 -0
  37. demo/README.md +87 -0
  38. demo/cds_only_sequences.csv +5 -0
  39. demo/create_csv.py +179 -0
  40. demo/create_demo_db.py +260 -0
  41. demo/demo_models.py +166 -0
  42. demo/docker-compose.yml +19 -0
  43. demo/init_postgres.sql +89 -0
  44. demo/init_railway_db.py +73 -0
  45. demo/mrna_sequences.csv +3 -0
  46. demo/utr_library.csv +7 -0
  47. models/README.md +162 -0
  48. models/__init__.py +32 -0
  49. models/base.py +447 -0
  50. 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: Mrna Design Studio
3
- emoji: 🏒
4
- colorFrom: red
5
- colorTo: pink
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ]