File size: 12,371 Bytes
914512c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
"""bioai.simulation.wet_lab -- Virtual wet-lab validation simulator.

Simulates the cellular knockdown pipeline for dsRNA biopesticide candidates
using a 6-stage Monte Carlo model. This is NOT a replacement for real wet-lab
testing β€” it's a computational screening tool that estimates which candidates
are most likely to succeed before spending real lab time and money.

The 6 stages (each modeled with biological literature-informed kinetics):

  1. DELIVERY  β€” fraction of applied dsRNA that reaches target pest cells.
     Modeled as a function of the PINN-predicted environmental half-life
     (longer half-life = more dsRNA survives to reach the pest).

  2. UPTAKE    β€” cellular uptake efficiency. Modeled as a function of
     dsRNA length (21-nt siRNAs uptake more efficiently than 200-nt
     precursors) and GC content (moderate GC = better uptake).

  3. DICER     β€” probability of being correctly processed by Dicer into
     the active 21-nt siRNA. Modeled using sequence features (no internal
     repeats, moderate GC = better Dicer processing).

  4. RISC      β€” guide-strand loading efficiency. Modeled using
     thermodynamic asymmetry (Reynolds rule: lower 5' antisense binding
     energy = better guide loading).

  5. CLEAVAGE  β€” target mRNA cleavage rate. Uses the CNN-predicted
     efficacy score as the base rate, with noise to simulate biological
     variability.

  6. PHENOTYPE β€” phenotypic response (mortality / growth reduction).
     Modeled as a Hill dose-response curve: knockdown -> phenotype.

The final knockdown percentage is the product of all 6 stages, with noise
added at each step. We run N=1000 Monte Carlo trials per candidate and
report the mean, 95% confidence interval, and probability of achieving
>70% knockdown (the threshold for a "functional" siRNA per Reynolds 2004).

Scientific basis:
  - Reynolds et al. 2004 (Nature Biotechnology) β€” siRNA efficacy rules
  - Schwarz et al. 2003 (Cell) β€” thermodynamic asymmetry determines guide strand
  - Fire et al. 1998 (Nature) β€” RNAi discovery, dose-response kinetics
  - Bhatt et al. 2004 (NAR) β€” Dicer processing preferences
  - Parrish et al. 2000 (Molecular Cell) β€” dsRNA uptake in C. elegans

Limitations:
  - The noise model is Gaussian; real biological variability is heavier-tailed.
  - The dose-response curve is a simple Hill function; real pest response
    varies by species, life stage, and environmental conditions.
  - Off-target effects are not modeled here (handled separately by the
    k-mer index in the ranker).
  - This simulation CANNOT replace real wet-lab validation. It's a
    screening tool to prioritize candidates for lab testing.
"""

from __future__ import annotations

import math
from dataclasses import dataclass, field
from typing import Dict, List

import numpy as np


# ─────────────────────────────────────────────────────────────────────────────
# Constants (literature-informed defaults)
# ─────────────────────────────────────────────────────────────────────────────
N_TRIALS = 1000
KNOCKDOWN_THRESHOLD = 0.70  # Reynolds 2004 "functional" threshold


@dataclass
class SimulationResult:
    """Result of a single candidate's wet-lab simulation."""
    sirna_seq: str
    mean_knockdown: float           # mean fraction knocked down (0-1)
    ci_low: float                   # 95% CI lower bound
    ci_high: float                  # 95% CI upper bound
    prob_above_70: float            # P(knockdown > 70%)
    # Per-stage mean efficiencies (0-1)
    delivery_efficiency: float
    uptake_efficiency: float
    dicer_efficiency: float
    risc_loading: float
    cleavage_rate: float
    phenotypic_response: float
    n_trials: int = N_TRIALS


# ─────────────────────────────────────────────────────────────────────────────
# WetLabSimulator
# ─────────────────────────────────────────────────────────────────────────────
class WetLabSimulator:
    """Monte Carlo simulator for the dsRNA cellular knockdown pipeline.

    Parameters
    ----------
    n_trials:
        Number of Monte Carlo trials per candidate (default 1000).
    rng_seed:
        Random seed for reproducibility (default 42).
    """

    def __init__(self, n_trials: int = N_TRIALS, rng_seed: int = 42):
        self.n_trials = n_trials
        self.rng = np.random.default_rng(rng_seed)

    def simulate_candidate(
        self,
        sirna_seq: str,
        cnn_efficacy: float,
        half_life_hours: float,
        reynolds_score: int = 8,
    ) -> SimulationResult:
        """Run the 6-stage Monte Carlo simulation for one candidate.

        Models an in vitro cell-culture screening assay (the standard first
        step in dsRNA biopesticide validation). In this context:
        - Delivery is via lipofection (80-95% efficient, not field spray)
        - The cellular machinery (Dicer, RISC) operates at high efficiency
        - The CNN efficacy is the primary driver of knockdown
        - Other stages act as moderate modifiers (0.7-1.0x)

        Parameters
        ----------
        sirna_seq:
            The 21-nt siRNA sequence.
        cnn_efficacy:
            CNN-predicted efficacy (0-1) β€” primary driver of knockdown.
        half_life_hours:
            PINN-predicted environmental half-life (modifies delivery stage).
        reynolds_score:
            Reynolds 2004 rule score (0-8). Higher = better siRNA design.
        """
        gc = self._gc_content(sirna_seq)
        has_repeats = self._has_internal_repeats(sirna_seq)

        knockdowns = np.zeros(self.n_trials)
        delivery_arr = np.zeros(self.n_trials)
        uptake_arr = np.zeros(self.n_trials)
        dicer_arr = np.zeros(self.n_trials)
        risc_arr = np.zeros(self.n_trials)
        cleavage_arr = np.zeros(self.n_trials)
        phenotype_arr = np.zeros(self.n_trials)

        for i in range(self.n_trials):
            # Stage 1: Delivery (lipofection in vitro) β€” 80-95% efficient
            # Longer half-life slightly improves delivery stability
            hl_bonus = (half_life_hours - 24) / (168 - 24) * 0.08  # 0-8% bonus
            delivery = 0.85 + hl_bonus + self.rng.normal(0, 0.04)
            delivery = np.clip(delivery, 0.70, 0.98)
            delivery_arr[i] = delivery

            # Stage 2: Uptake β€” 75-95%, GC-dependent
            # Moderate GC (0.3-0.6) is optimal for uptake
            gc_factor = 1.0 - abs(gc - 0.45) * 0.8
            gc_factor = np.clip(gc_factor, 0.7, 1.0)
            uptake = (0.80 * gc_factor) + self.rng.normal(0, 0.05)
            uptake = np.clip(uptake, 0.60, 0.95)
            uptake_arr[i] = uptake

            # Stage 3: Dicer processing β€” 80-95%, Reynolds-dependent
            dicer_base = 0.75 + (reynolds_score / 8.0) * 0.20
            if has_repeats:
                dicer_base *= 0.85
            dicer = dicer_base + self.rng.normal(0, 0.04)
            dicer = np.clip(dicer, 0.65, 0.97)
            dicer_arr[i] = dicer

            # Stage 4: RISC loading β€” 70-95%, thermodynamic asymmetry dependent
            risc_base = 0.65 + (reynolds_score / 8.0) * 0.30
            risc = risc_base + self.rng.normal(0, 0.06)
            risc = np.clip(risc, 0.55, 0.95)
            risc_arr[i] = risc

            # Stage 5: Target cleavage β€” driven by CNN efficacy
            # The CNN predicts the intrinsic cleavage efficiency; add biological noise
            cleavage = cnn_efficacy + self.rng.normal(0, 0.08)
            cleavage = np.clip(cleavage, 0.20, 0.98)
            cleavage_arr[i] = cleavage

            # Stage 6: Phenotypic response β€” mRNA knockdown -> phenotype
            # In cell culture, phenotype tracks mRNA knockdown closely (R^2 ~ 0.85)
            # Use a soft saturating function rather than full Hill
            cumulative = delivery * uptake * dicer * risc * cleavage
            # Soft saturation: phenotype = cumulative^0.85 (sublinear, realistic)
            phenotype = cumulative ** 0.85 + self.rng.normal(0, 0.04)
            phenotype = np.clip(phenotype, 0.0, 0.95)
            phenotype_arr[i] = phenotype

            knockdowns[i] = phenotype

        mean_kd = float(np.mean(knockdowns))
        ci_low = float(np.percentile(knockdowns, 2.5))
        ci_high = float(np.percentile(knockdowns, 97.5))
        prob_above_70 = float(np.mean(knockdowns > KNOCKDOWN_THRESHOLD))

        return SimulationResult(
            sirna_seq=sirna_seq,
            mean_knockdown=mean_kd,
            ci_low=ci_low,
            ci_high=ci_high,
            prob_above_70=prob_above_70,
            delivery_efficiency=float(np.mean(delivery_arr)),
            uptake_efficiency=float(np.mean(uptake_arr)),
            dicer_efficiency=float(np.mean(dicer_arr)),
            risc_loading=float(np.mean(risc_arr)),
            cleavage_rate=float(np.mean(cleavage_arr)),
            phenotypic_response=float(np.mean(phenotype_arr)),
        )

    def simulate_batch(
        self,
        candidates: List[Dict],
        pest_species: str = "unknown",
    ) -> List[SimulationResult]:
        """Run the simulation for a batch of candidates.

        Each candidate dict should contain:
            - sirna_seq: the 21-nt sequence
            - efficacy: CNN-predicted efficacy (0-1)
            - half_life_hours: PINN-predicted half-life
            - reynolds_score (optional): 0-8, defaults to 8 if missing
        """
        results = []
        for cand in candidates:
            seq = cand.get("sirna_seq", "")
            eff = cand.get("efficacy", 0.5)
            hl = cand.get("half_life_hours", 48.0)
            rs = cand.get("reynolds_score", 8)
            result = self.simulate_candidate(seq, eff, hl, rs)
            results.append(result)
        return results

    # ─── Sequence helpers ────────────────────────────────────────────────
    @staticmethod
    def _gc_content(seq: str) -> float:
        seq = seq.upper().replace("U", "T")
        if not seq:
            return 0.0
        return (seq.count("G") + seq.count("C")) / len(seq)

    @staticmethod
    def _has_internal_repeats(seq: str) -> bool:
        """Check for internal repeats (>4 consecutive identical bases)."""
        seq = seq.upper()
        for i in range(len(seq) - 4):
            if seq[i] == seq[i + 1] == seq[i + 2] == seq[i + 3] == seq[i + 4]:
                return True
        return False


# ─────────────────────────────────────────────────────────────────────────────
# Result serialization
# ─────────────────────────────────────────────────────────────────────────────
def result_to_dict(result: SimulationResult) -> Dict:
    """Convert a SimulationResult to a JSON-serializable dict."""
    return {
        "sirna_seq": result.sirna_seq,
        "mean_knockdown": round(result.mean_knockdown, 4),
        "ci_low": round(result.ci_low, 4),
        "ci_high": round(result.ci_high, 4),
        "prob_above_70": round(result.prob_above_70, 4),
        "delivery_efficiency": round(result.delivery_efficiency, 4),
        "uptake_efficiency": round(result.uptake_efficiency, 4),
        "dicer_efficiency": round(result.dicer_efficiency, 4),
        "risc_loading": round(result.risc_loading, 4),
        "cleavage_rate": round(result.cleavage_rate, 4),
        "phenotypic_response": round(result.phenotypic_response, 4),
        "n_trials": result.n_trials,
    }