#بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ #Bismillāhi ar‑Raḥmāni ar‑Raḥīm. #"In the name of Allah, the Most Merciful, the Most Compassionate." import gradio as gr import torch import torch.nn as nn import torch.nn.functional as F import matplotlib.pyplot as plt import numpy as np import re import os import pandas as pd import tempfile from captum.attr import LayerIntegratedGradients # ---------- NCBI streaming (zero‑disk) ---------- from Bio import Entrez Entrez.email = "zammy@gmail.com.com" # NCBI requires an email def fetch_ncbi(accession): """Return the FASTA sequence for a given NCBI accession, or an error message.""" if not accession or not accession.strip(): return "", gr.update(visible=True, value="⚠️ Please enter an NCBI accession number.") try: handle = Entrez.efetch(db="nucleotide", id=accession.strip(), rettype="fasta", retmode="text") record = handle.read() handle.close() lines = record.splitlines() seq = "".join(line.strip() for line in lines if not line.startswith(">")) if not seq: return "", gr.update(visible=True, value="❌ No sequence found for that accession.") return seq.upper(), gr.update(visible=True, value=f"✅ Loaded {len(seq)} bp from NCBI.") except Exception as e: return "", gr.update(visible=True, value=f"❌ NCBI fetch error: {e}") # ========================================== # 1. BIOPHYSICAL TENSOR FUSION MODEL (128‑channel, real miCLIP) # ========================================== device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') class CrossScaleFusionGate(nn.Module): def __init__(self, channels=128): super().__init__() self.query_conv = nn.Conv1d(channels, channels//4, 1) self.key_conv = nn.Conv1d(channels, channels//4, 1) self.value_conv = nn.Conv1d(channels, channels, 1) self.gamma = nn.Parameter(torch.zeros(1)) def forward(self, source, target): B, C, L = source.shape Q = self.query_conv(source).view(B, -1, L).permute(0,2,1) K = self.key_conv(target).view(B, -1, L) attn = F.softmax(torch.bmm(Q, K), dim=-1) V = self.value_conv(target).view(B, -1, L) out = torch.bmm(V, attn.permute(0,2,1)).view(B, C, L) return source + self.gamma * out class BiophysicalTensorFusionModel(nn.Module): def __init__(self, channels=128): super().__init__() biophysical_matrix = torch.tensor([ [0.0, 0.0, 0.0], [1.0, -1.0, 0.5], [-1.0, -1.0, -0.5], [-1.0, 1.0, 2.5], [1.0, 1.0, -1.0] ]) self.embedding = nn.Embedding.from_pretrained(biophysical_matrix, freeze=False) self.local_path = nn.Conv1d(3, channels, kernel_size=3, padding=1) self.flank_path = nn.Conv1d(3, channels, kernel_size=5, padding=4, dilation=2) self.struct_path = nn.Conv1d(3, channels, kernel_size=5, padding=8, dilation=4) self.fuse_local = CrossScaleFusionGate(channels) self.fuse_flank = CrossScaleFusionGate(channels) self.layer_norm = nn.LayerNorm(channels * 3) self.fc_contrast = nn.Linear(channels * 3, 1) def forward(self, x): x_emb = self.embedding(x).transpose(1, 2) c1 = self.local_path(x_emb) c2 = self.flank_path(x_emb) c3 = self.struct_path(x_emb) c1 = self.fuse_local(c1, c3) c2 = self.fuse_flank(c2, c3) p1 = F.max_pool1d(F.pad(c1, (0, 1)), kernel_size=2, stride=1) p2 = F.max_pool1d(F.pad(c2, (0, 1)), kernel_size=2, stride=1) p3 = F.max_pool1d(F.pad(c3, (0, 1)), kernel_size=2, stride=1) combined = torch.cat([p1, p2, p3], dim=1).transpose(1, 2) return self.fc_contrast(F.relu(self.layer_norm(combined))).squeeze(-1) # Instantiate CNN model (uses default channels=128) model = BiophysicalTensorFusionModel().to(device).eval() if os.path.exists("EpiRNA_Biophysical_Master.pt"): try: state_dict = torch.load("EpiRNA_Biophysical_Master.pt", map_location=device, weights_only=False) model.load_state_dict(state_dict, strict=False) print("✅ CNN model loaded.") except Exception as e: print(f"⚠️ Could not load CNN checkpoint: {e}") # ========================================== # 2. ADAPTIVE PROCESSING & STABILIZATION # ========================================== def compute_advanced_calibrated_profile(raw_deltas): global_std = torch.std(raw_deltas) + 1e-4 raw_deltas = torch.clamp(raw_deltas, min=-2.0, max=2.0) calibrated = torch.zeros_like(raw_deltas) for i in range(len(raw_deltas)): start = max(0, i - 6) end = min(len(raw_deltas), i + 7) local_ctx = raw_deltas[start:end] blended_std = (torch.std(local_ctx) * 0.3) + (global_std * 0.7) + 1e-4 z_score = (raw_deltas[i] - torch.mean(local_ctx)) / blended_std calibrated[i] = torch.clamp((torch.sigmoid(z_score) - 0.5) * 2.0, min=0.0) return calibrated.cpu().numpy() # ========================================== # 3. HELPER FUNCTIONS # ========================================== def calc_gc_content(sequence, window=15): gc_vals = [] half = window // 2 for i in range(len(sequence)): sub = sequence[max(0, i - half) : min(len(sequence), i + half + 1)] gc_vals.append((sub.count('G') + sub.count('C')) / len(sub)) return gc_vals def find_drach_motifs(sequence): pattern = r'[AGU][AG]AC[ACU]' matches = list(re.finditer(pattern, sequence)) highlighted_seq = sequence for m in reversed(matches): start, motif = m.start(), m.group() highlighted_seq = ( highlighted_seq[:start] + f"**{motif}**" + highlighted_seq[start+5:] ) motifs_text = ", ".join( [f"{m.group()} (Pos {m.start()})" for m in matches] ) if matches else "None detected." return motifs_text, highlighted_seq # ========================================== # 4. ENHANCED PREDICT # ========================================== def predict(raw_seq, threshold=0.45): raw_seq = raw_seq.upper().strip().replace('T', 'U') illegal = set(raw_seq) - {'A', 'U', 'C', 'G'} if illegal: return None, f"
Architecture: Biophysical Tensor Fusion (variable‑length)
Max Contrast: {scores[raw_peak]:.4f}
Sequence Map: {highlighted_seq}
Canonical DRACH Motifs: {motifs_text}
Explanation window: first 41 bases of your input.
Positive bars (indigo) = increase catalytic boundary signal.
Negative bars (red) = decrease it.
Explainability failed: {e}
" # ========================================== # 7. GLASSMORPHISM FRONTEND THEME # ========================================== glass_theme = gr.themes.Soft( primary_hue="indigo", neutral_hue="slate" ).set( body_background_fill="#f8fafc", body_background_fill_dark="#f8fafc", background_fill_primary="rgba(255, 255, 255, 0.85)", background_fill_primary_dark="rgba(255, 255, 255, 0.85)", background_fill_secondary="rgba(255, 255, 255, 0.6)", background_fill_secondary_dark="rgba(255, 255, 255, 0.6)", border_color_primary="rgba(203, 213, 225, 0.6)", border_color_primary_dark="rgba(203, 213, 225, 0.6)", block_background_fill="rgba(255, 255, 255, 0.7)", block_background_fill_dark="rgba(255, 255, 255, 0.7)", block_title_text_color="#111827", block_title_text_color_dark="#111827", block_label_text_color="#374151", block_label_text_color_dark="#374151", body_text_color="#1f2937", body_text_color_dark="#1f2937", input_background_fill="#ffffff", input_background_fill_dark="#ffffff", ) custom_css = """ /* ========== GLOBAL LAYOUT ========== */ :root { --font-mono: 'DM Serif Display', 'JetBrains Mono', 'Courier New', Courier, monospace; } .gradio-container { background: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 100%) !important; font-family: var(--font-mono) !important; letter-spacing: -0.02em !important; min-height: 100vh !important; padding: 2rem !important; } .gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container p, .gradio-container label, .gradio-container span, .gradio-container button { font-family: var(--font-mono) !important; color: #1f2937 !important; } footer { display: none !important; } /* ========== INPUTS & TEXTAREAS ========== */ .gradio-container textarea, .gradio-container input[type="text"], .gradio-container input[type="number"], .gradio-container .block { background: rgba(255, 255, 255, 0.75) !important; backdrop-filter: blur(20px) !important; -webkit-backdrop-filter: blur(20px) !important; color: #111827 !important; border: 1px solid rgba(255, 255, 255, 0.6) !important; border-radius: 14px !important; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.02), inset 0 1px 0 rgba(255, 255, 255, 0.6) !important; transition: all 0.25s ease !important; } .gradio-container textarea:focus, .gradio-container input:focus, .gradio-container .block:focus-within { border-color: rgba(0, 0, 0, 0.2) !important; background: rgba(255, 255, 255, 0.95) !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04) !important; outline: none !important; } /* ========== BUTTONS ========== */ .gradio-container button.primary, .gradio-container .gr-button-primary { background: #000000 !important; color: #ffffff !important; border-radius: 18px !important; padding: 12px 24px !important; font-weight: 500 !important; border: none !important; cursor: pointer !important; transition: all 0.2s ease !important; } .gradio-container button.primary:hover, .gradio-container .gr-button-primary:hover { background: #ff3b30 !important; transform: translateY(-1px) !important; box-shadow: 0 6px 20px rgba(255, 59, 48, 0.2) !important; } .gradio-container button.primary:active, .gradio-container .gr-button-primary:active { transform: translateY(0px) !important; } /* ========== TABS ========== */ .gradio-container .tabs { border: none !important; background: transparent !important; } .gradio-container .tab-nav { border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important; padding-left: 0 !important; gap: 8px !important; display: flex !important; } .gradio-container .tab-nav button { color: #86868b !important; font-weight: 500 !important; background: transparent !important; font-size: 0.85rem !important; padding: 12px 20px !important; border-radius: 8px 8px 0 0 !important; border: none !important; transition: all 0.2s ease !important; } .gradio-container .tab-nav button:hover { background: rgba(255, 255, 255, 0.4) !important; color: #000000 !important; } .gradio-container .tab-nav button.selected { color: #000000 !important; border-bottom: 2px solid #ff3b30 !important; background: rgba(255, 255, 255, 0.8) !important; } /* ========== TOOLTIPS ========== */ .pro-tooltip { position: relative; display: inline-block; cursor: help; border-bottom: 2px dotted #ff3b30; font-weight: 600; color: #000000; } .pro-tooltip .tooltip-text { visibility: hidden; width: max-content; max-width: 300px; background: rgba(0, 0, 0, 0.95); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: #ffffff !important; padding: 12px 16px; border-radius: 12px; position: absolute; z-index: 999; bottom: 130%; left: 50%; transform: translateX(-50%) translateY(8px); opacity: 0; transition: all 0.2s ease; font-size: 0.8rem; font-weight: 400; line-height: 1.4; pointer-events: none; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15); } .pro-tooltip:hover .tooltip-text { visibility: visible; opacity: 1; transform: translateX(-50%) translateY(0); } /* ========== TABLES ========== */ .gradio-container table { border-radius: 12px !important; border-collapse: collapse !important; overflow: hidden !important; background: #ffffff !important; border: 1px solid rgba(0, 0, 0, 0.05) !important; } .gradio-container tbody tr:hover td { background-color: rgba(0, 0, 0, 0.03) !important; transition: background-color 0.15s ease !important; } /* ========== INPUT LABEL FIX ========== */ .gradio-container label, .gradio-container .label, .gradio-container [data-testid="block-info"], .gradio-container span[class*="label"] { background: transparent !important; background-color: transparent !important; box-shadow: none !important; border: none !important; padding-left: 0 !important; padding-right: 0 !important; color: #1d1d1f !important; font-weight: 600 !important; font-size: 0.9rem !important; } .gradio-container label span, .gradio-container .label span { color: #1d1d1f !important; background: transparent !important; background-color: transparent !important; } /* ========== BLOCK GLASSMORPHISM ========== */ .gradio-container .block { background: rgba(255, 255, 255, 0.75) !important; border: 1px solid rgba(0, 0, 0, 0.08) !important; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.02) !important; } /* ========== TABLE HOVER ========== */ table tr:hover td { background-color: #e0e7ff !important; transition: background-color 0.2s ease; } /* ========== RADIO BUTTONS ========== */ .gr-radio-group .gr-radio { background: rgba(255,255,255,0.7) !important; border-radius: 8px !important; padding: 6px 12px !important; margin: 4px !important; border: 1px solid #cbd5e1 !important; transition: all 0.2s ease; } .gr-radio-group .gr-radio.selected { background: #4f46e5 !important; color: #ffffff !important; border-color: #4f46e5 !important; } .gr-radio-group label { cursor: pointer; font-size: 0.9rem !important; } /* NCBI fetch button – matching glassmorphism theme */ /* Center the NCBI fetch button */ #fetch-btn { display: block !important; margin: 0 auto !important; } """ with gr.Blocks(title="EpiRNA-C") as app: with gr.Row(): with gr.Column(scale=4): gr.HTML("""Decoding RNA Catalytic Boundaries at Single‑Nucleotide Resolution
Traditional deep learning models for RNA modifications overfit to lab-specific technical noise (like GC-content biasA common laboratory artifact where sequencing machines preferentially read sequences rich in Guanine (G) and Cytosine (C), tricking AI models into correlating GC% with RNA modifications.). They fail to generalize across unseen datasets.
EpiRNA leverages a DANNDomain Adversarial Neural Network. trained on SSBSynthetic Sandbox Bootstrapping.. By mathematically stripping away technical batch artifacts, it learns true causal biology.
Epitranscriptomic Boundary Contrast Scoring (EBCSA zero-shot mathematical probe that calculates the exact single-nucleotide derivative of an AI model's confidence.) slides a synthetic mask across the sequence to calculate the mathematical derivative of the model's confidence. The peak contrast deltaThe highest point on the blue graph line. reveals the exact single-nucleotide catalytic boundary the AI relies upon.
EpiRNA replaces traditional one‑hot nucleotide encoding with a 3‑dimensional biophysical vector for each base, directly embedding the chemical properties that govern RNA catalysis:
| Base | H‑Bond Potential | Stacking Energy | Solvent Accessibility |
|---|---|---|---|
| A | +1.0 | −1.0 | +0.5 |
| U/T | −1.0 | −1.0 | −0.5 |
| C | −1.0 | +1.0 | +2.5 |
| G | +1.0 | +1.0 | −1.0 |
This physical grounding allows the model to inherently discriminate functional cytosine‑containing motifs (like DRACH) from inert decoys, without requiring explicit motif annotation.
The sequence is processed by three parallel 1D‑convolutional arms:
All arms use MaxPool1d to prevent background smearing at transition boundaries,
then are concatenated and normalised before the final contrast head.
Raw delta scores are calibrated with a local‑global variance blender: a Z‑score is computed using a blended standard deviation (30% local window, 70% global), then mapped to [0,1] via a shifted sigmoid. This eliminates logit saturation and ensures stable, comparable scores across sequences of any length.
A final production noise gate (threshold = 0.45) zeroes out low‑confidence background fluctuations caused by abrupt GC‑content transitions, leaving only genuine catalytic peaks in the visualisation.
Instead of simply reporting the highest score, the pipeline searches for canonical
[AGU][AG]AC[ACU] motifs and pinpoints the modifying adenosine
(position +2 from the motif start). If no DRACH motif is found, it falls back to
the centre of high‑score plateaus (≥0.85). This biologically informed peak‑picking
rejects false positives from non‑functional patterns.
The model accepts any sequence ≥41 bp by sliding a 41‑nucleotide window with overlapping averaging, making it suitable for full‑length transcripts, genomic RNA fragments, and synthetic constructs.
Model weights pre‑trained on curated epi‑transcriptomic datasets. For technical details and benchmarks, see the project repository. """) # Unified logic: Single button triggers everything def run_all(seq, threshold): fig, res, mot = predict(seq, threshold) exp_fig, exp_text = run_explainer(seq) return fig, res, mot, exp_fig, exp_text run_btn.click(run_all, inputs=[seq_input, threshold_radio], outputs=[out_plot, out_res, out_mot, exp_plot, exp_res]) batch_btn.click(process_batch, inputs=[batch_file], outputs=[batch_download, batch_status]) fetch_btn.click( fn=fetch_ncbi, inputs=[ncbi_acc], outputs=[seq_input, fetch_status] ) app.queue().launch(theme=glass_theme, css=custom_css)