File size: 29,757 Bytes
0f54414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
"""Master figure generator for the Plant DNA Designer research paper.



Regenerates ALL eight figures with consistent styling and β€” critically β€”

figure numbers that match the manuscript (figures are numbered in order of

first reference in the text). Committing this script makes every figure

reproducible and keeps the in-image titles in lock-step with the captions.



    python docs/make_figures.py          # regenerate all 8 PNGs at 300 dpi



Data sources: benchmark means are the verbatim output of `cd cool &&

python -m benchmark`; the species-coverage matrix and clade assignments are

read live from cool/core/species.py and cool/core/codons.py so the figure can

never drift from the code.

"""
import os
import sys
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
from matplotlib.lines import Line2D

HERE = os.path.dirname(os.path.abspath(__file__))
FIGDIR = os.path.join(HERE, "figures")
os.makedirs(FIGDIR, exist_ok=True)

# Make the real science package importable so figures can be driven by actual
# tool output (real Kozak tables, real elongation profiles, real GA runs) rather
# than synthetic placeholders.
sys.path.insert(0, os.path.join(HERE, "..", "cool"))

plt.rcParams.update({
    "font.size": 11,
    "axes.facecolor": "#eef2f7",
    "figure.facecolor": "white",
    "savefig.facecolor": "white",
})

BLUE, DBLUE, GREEN, DGREEN = "#1f9bf0", "#1f5fb0", "#3a9b4e", "#1d6b2e"
PURPLE, RED, ORANGE = "#8e26b8", "#c0392b", "#e8730c"


def _title(fig, n, text):
    # Descriptive title only β€” the figure NUMBER is supplied by the manuscript
    # caption ("Figure N."), so embedding it here would duplicate it. The `n`
    # argument is kept for call-site clarity and filename mapping.
    fig.suptitle(text, fontsize=15, fontweight="bold", y=0.99)


def _panel(ax, letter):
    """Bold (A)/(B) panel label at the top-left of an axes (journal convention)."""
    ax.text(-0.04, 1.11, f"({letter})", transform=ax.transAxes,
            fontsize=16, fontweight="bold", va="bottom", ha="right", clip_on=False)


def _save(fig, name):
    path = os.path.join(FIGDIR, name)
    fig.savefig(path, dpi=300, bbox_inches="tight")
    plt.close(fig)
    print("wrote", os.path.relpath(path, HERE))


# ── Figure 1 β€” system architecture ───────────────────────────────────────────
def fig1_architecture():
    fig, ax = plt.subplots(figsize=(13.5, 7.6))
    ax.set_xlim(0, 100); ax.set_ylim(0, 100); ax.axis("off")
    ax.set_facecolor("white")
    _title(fig, 1, "Plant DNA Designer: System Architecture")

    def box(x, y, w, h, text, fc, tc="white", fs=11):
        ax.add_patch(FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.4,rounding_size=2",
                                    fc=fc, ec="white", lw=1.5))
        ax.text(x + w / 2, y + h / 2, text, ha="center", va="center",
                color=tc, fontsize=fs, fontweight="bold")

    def arrow(x1, y1, x2, y2):
        ax.add_patch(FancyArrowPatch((x1, y1), (x2, y2), arrowstyle="-|>",
                                     mutation_scale=14, color="#333", lw=1.4))

    # input layer
    inp = [("Crop Species\n& Trait Selection"), ("Target Protein\n(Effector)"),
           ("Codon Table\n& Parameters"), ("GA Settings\n(pop / gen / ΞΌ)")]
    for i, t in enumerate(inp):
        box(2 + i * 24.5, 84, 22, 12, t, BLUE)
    # engine layer
    box(2, 60, 46, 16, "Genetic Algorithm Engine\n(NSGA-II capable Β· liability-directed mutation)", DBLUE)
    box(52, 60, 46, 16, "Multi-Objective Fitness\n(CAI Β· tAI Β· Harmony Β· Structure Β· Safety Β· GC)", DGREEN)
    # analysis modules
    mods = [("Codon\nOptimizer\n(CAI / tAI)", GREEN), ("mRNA\nStructure\n(5β€² open)", GREEN),
            ("Translation\nDynamics", GREEN), ("CRISPR\ngRNA\nDesign", PURPLE),
            ("Safety\nScanner", RED)]
    for i, (t, c) in enumerate(mods):
        box(2 + i * 19.6, 34, 18, 18, t, c, fs=10)
    # output
    outs = ["Optimised CDS\n(FASTA / GenBank)", "Expression\nCassette",
            "Pathway\nBalance Map", "Safety\nReport"]
    for i, t in enumerate(outs):
        box(2 + i * 24.5, 10, 22, 14, t, ORANGE, fs=10)

    arrow(24, 84, 24, 76.5); arrow(36, 84, 25, 76.5)
    arrow(74, 84, 74, 76.5)
    arrow(25, 60, 25, 52.5); arrow(74, 60, 74, 52.5)
    arrow(11, 34, 13, 24); arrow(40, 34, 36, 24)
    arrow(60, 34, 62, 24); arrow(89, 34, 86, 24)
    _save(fig, "fig1_architecture.png")


# ── Figure 2 β€” multi-objective fitness ───────────────────────────────────────
def fig2_fitness():
    # REAL normalised sub-scores computed for a rice DREB2A CAI-max sequence and
    # genuine PDD default / folding designs (same normalisers the GA uses).
    import math, random
    from core.mechanisms import EFFECTORS
    from core.codons import get_codon_usage
    from core.designer import AdvancedDnaDesigner
    from core.analyzer import DnaAnalyzer
    from core.expression import TransgeneSafetyScanner, translation_dynamics_score
    random.seed(0); np.random.seed(0)
    _prot = EFFECTORS["DREB2A"]["protein"]; _cu = get_codon_usage("rice"); _tgc = 45.0

    def _axes(dna):
        a = DnaAnalyzer(dna); gc = a.gc_content()
        cl = lambda v: max(0.0, min(1.0, v))
        return [cl(a.calculate_cai(_cu)), cl(a.tai_score("rice")), cl(a.codon_harmony(_cu)),
                math.exp(-((gc - _tgc) / 8.0) ** 2),
                cl(1 + a.start_codon_structure_penalty() / 0.4),
                cl(a.body_structure_score() / 0.4),
                1 - cl(TransgeneSafetyScanner(dna).scan()["penalty"] / 6.0),
                cl(translation_dynamics_score(dna, _cu))]

    _dd = AdvancedDnaDesigner(_prot, "rice", 40, 60, "rice", harmonize=False)
    _df = AdvancedDnaDesigner(_prot, "rice", 40, 60, "rice", harmonize=True)
    caimax = _axes(_dd._create_optimal_individual())
    default = _axes(_dd.generate_sequence(_tgc, [], ["AAAAAA"], mutation_rate=0.15)[0])
    folding = _axes(_df.generate_sequence(_tgc, [], ["AAAAAA"], mutation_rate=0.15)[0])

    fig = plt.figure(figsize=(13.5, 5.6))
    _title(fig, 2, "Multi-Objective Fitness Function Design")
    axR = fig.add_subplot(1, 2, 1, projection="polar")
    axB = fig.add_subplot(1, 2, 2)

    labels = ["CAI", "tAI", "Harmony", "GC\nFidelity", "Start\nOpen",
              "Body\nStruct", "Safety", "Dyn\nScore"]
    ang = np.linspace(0, 2 * np.pi, len(labels), endpoint=False).tolist()
    ang += ang[:1]
    for vals, name, col in [(caimax, "CAI-max", RED),
                            (default, "PDD Default", DBLUE),
                            (folding, "PDD Folding", GREEN)]:
        v = vals + vals[:1]
        axR.plot(ang, v, color=col, lw=2, label=name)
        axR.fill(ang, v, color=col, alpha=0.12)
    axR.set_xticks(ang[:-1]); axR.set_xticklabels(labels, fontsize=9)
    axR.set_ylim(0, 1.05); axR.set_yticks([0.25, 0.5, 0.75, 1.0])
    axR.set_yticklabels(["0.25", "0.5", "0.75", "1.0"], fontsize=7.5)
    axR.set_title("Normalised Sub-Score Profile", fontsize=12, pad=18)
    axR.legend(loc="upper right", bbox_to_anchor=(1.18, 1.16), fontsize=8.5)

    # Read the REAL weight table live from the code so this panel can never drift
    # from the GA. Display names + tier colours are derived from the weights.
    _NAMES = {
        "forbidden_motifs": "Forbidden Motifs", "cai": "CAI",
        "transgene_safety": "Transgene Safety", "gc_fidelity": "GC Fidelity",
        "start_openness": "Start Openness", "tai": "tAI",
        "plantcare_motifs": "PlantCARE Motifs", "hexamer_profile": "Hexamer Profile",
        "codon_harmony": "Codon Harmony", "restriction_sites": "Restriction Sites",
        "codon_pair": "Codon Pair", "gc3": "GC3 Wobble", "mtdr": "MTDR",
        "codon_ramp": "Codon Ramp", "kozak": "Kozak", "body_structure": "Body Structure",
        "translation_dynamics": "Translation Dynamics", "stability_motifs": "Stability Motifs",
        "homopolymer": "Homopolymer", "translation_rhythm": "Folding Rhythm",
    }
    _tier = lambda w: RED if w >= 90 else ("#b07c1f" if w >= 70 else GREEN)
    W = AdvancedDnaDesigner._WEIGHTS
    n_active = sum(1 for w in W.values() if w > 0)
    top = sorted(((w, _NAMES.get(k, k)) for k, w in W.items() if w > 0), reverse=True)[:10]
    objs = [(name, w, _tier(w)) for w, name in top]
    objs = objs[::-1]
    names = [o[0] for o in objs]; vals = [o[1] for o in objs]; cols = [o[2] for o in objs]
    y = np.arange(len(objs))
    axB.barh(y, vals, color=cols, edgecolor="white")
    for yi, v in zip(y, vals):
        axB.text(v + 2, yi, str(v), va="center", fontsize=9)
    axB.set_yticks(y); axB.set_yticklabels(names, fontsize=9.5)
    axB.set_xlabel("Relative Weight"); axB.set_xlim(0, 145)
    axB.set_title(f"Fitness Objective Weights\n(top 10 of {n_active} objectives)", fontsize=12)
    axB.grid(axis="x", color="white"); axB.set_axisbelow(True)
    handles = [Line2D([0], [0], color=RED, lw=8, label="Critical (β‰₯90)"),
               Line2D([0], [0], color="#b07c1f", lw=8, label="High (70–89)"),
               Line2D([0], [0], color=GREEN, lw=8, label="Medium (<70)")]
    axB.legend(handles=handles, fontsize=8.5, loc="lower right")
    fig.tight_layout(rect=[0, 0, 1, 0.95])
    _panel(axR, "A"); _panel(axB, "B")
    _save(fig, "fig2_fitness.png")


# ── Figure 3 β€” translation dynamics ──────────────────────────────────────────
def fig3_dynamics():
    # REAL data: a rice DREB2A CAI-max sequence vs a genuine PDD folding-mode
    # design; velocity from elongation_profile, boundaries from the real
    # Kyte-Doolittle domain-boundary predictor.
    import random
    from core.mechanisms import EFFECTORS
    from core.codons import get_codon_usage
    from core.designer import AdvancedDnaDesigner
    from core.expression import (elongation_profile, ideal_speed_schedule,
                                 predict_domain_boundaries, _KYTE_DOOLITTLE)
    random.seed(0); np.random.seed(0)
    protein = EFFECTORS["DREB2A"]["protein"]
    cu = get_codon_usage("rice")
    d = AdvancedDnaDesigner(protein, "rice", population_size=40, generations=60,
                            codon_table="rice", harmonize=True)
    cai_seq = d._create_optimal_individual()
    pdd_seq = d.generate_sequence(45.0, [], ["AAAAAA"], mutation_rate=0.15)[0]
    cprof = np.array(elongation_profile(cai_seq, cu))
    pprof = np.array(elongation_profile(pdd_seq, cu))
    n = int(min(len(cprof), len(pprof)))
    bnds = [b for b in predict_domain_boundaries(protein) if b < n]
    sched = np.array(ideal_speed_schedule(n, bnds))

    def smooth(a, k=5):
        return np.convolve(a, np.ones(k) / k, mode="same")

    fig, (axL, axR) = plt.subplots(1, 2, figsize=(13.5, 5.4))
    _title(fig, 3, "Translation Dynamics & Domain Boundary Model")
    x = np.arange(n)
    axL.axvspan(0, 25, color="#f4d8b0", alpha=0.5, label="Ramp zone")
    axL.plot(x, sched, "--", color="#888", lw=2, label="Ideal schedule")
    axL.plot(x, smooth(pprof[:n]), color=DBLUE, lw=1.8, label="PDD (folding mode)")
    axL.plot(x, smooth(cprof[:n]), color=RED, lw=1.2, alpha=0.85, label="CAI-max")
    for b in bnds:
        axL.axvline(b, color=GREEN, ls=":", lw=1)
    axL.set_xlabel("Codon position"); axL.set_ylabel("Relative elongation speed (smoothed)")
    axL.set_title("Ribosome Velocity Trajectory\n(real: DREB2A, rice)", fontsize=12)
    axL.set_ylim(0, 1.15); axL.legend(fontsize=8.5, loc="lower right")
    axL.grid(color="white"); axL.set_axisbelow(True)

    h = np.array([_KYTE_DOOLITTLE.get(a, 0.0) for a in protein])
    hs = smooth(h, 9)
    aa = np.arange(len(protein))
    axR.axhline(hs.mean(), color="#888", ls=":", lw=1, label="mean hydropathy")
    axR.plot(aa, hs, color=DBLUE, lw=2, label="Smoothed hydropathy")
    bb = [b for b in bnds if b < len(protein)]
    for b in bb:
        axR.axvline(b, color=GREEN, ls="--", lw=1.2)
    axR.scatter(bb, [hs[b] for b in bb], color=GREEN, zorder=5, s=36,
                label="Predicted linker (pause)")
    axR.set_xlabel("Amino acid position"); axR.set_ylabel("Kyte–Doolittle hydropathy")
    axR.set_title("Domain Boundary Detection\n(real: predict_domain_boundaries)", fontsize=12)
    axR.legend(fontsize=8.5, loc="upper right"); axR.grid(color="white"); axR.set_axisbelow(True)
    fig.tight_layout(rect=[0, 0, 1, 0.94])
    _panel(axL, "A"); _panel(axR, "B")
    _save(fig, "fig3_dynamics.png")


# ── Figure 4 β€” Kozak context ─────────────────────────────────────────────────
def fig4_kozak():
    # Plot PDD's REAL clade Kozak tables (softmax of the model's log-odds weights
    # β†’ a genuine per-position nucleotide-preference distribution).
    import math
    from core.expression import (_KOZAK_WEIGHTS_DICOT as KD,
                                 _KOZAK_WEIGHTS_MONOCOT as KM)
    fig, (axD, axM) = plt.subplots(1, 2, figsize=(14.0, 5.6))
    _title(fig, 4, "Species-Specific Kozak Translation Initiation Context")
    # Columns are the offsets the code actually scores: -6..-1 upstream and +4
    # (offset 3 = first base after the ATG). The ATG itself is fixed.
    labels = ["-6", "-5", "-4", "-3", "-2", "-1", "AUG", "+4"]
    offsets = {"-6": -6, "-5": -5, "-4": -4, "-3": -3, "-2": -2, "-1": -1, "+4": 3}
    NT = ["A", "C", "G", "U"]                       # mRNA; the code stores T
    cols = {"A": RED, "C": BLUE, "G": GREEN, "U": ORANGE}

    def prefs(table, off):
        w = table[off]
        raw = {nt: w.get("T" if nt == "U" else nt, min(w.values())) for nt in NT}
        e = {nt: math.exp(raw[nt]) for nt in NT}
        s = sum(e.values())
        return {nt: e[nt] / s for nt in NT}

    def stacked(ax, table, title):
        x = np.arange(len(labels))
        bottom = np.zeros(len(labels))
        for nt in NT:
            vals = [prefs(table, offsets[p])[nt] if p != "AUG" else 0 for p in labels]
            ax.bar(x, vals, 0.8, bottom=bottom, color=cols[nt], label=nt, edgecolor="white", lw=0.3)
            bottom += np.array(vals)
        ax.axvline(6, color="black", ls="--", lw=1.6)
        ax.text(6, 1.06, "AUG", ha="center", fontweight="bold")
        ax.set_xticks(x); ax.set_xticklabels(labels)
        ax.set_ylim(0, 1.1)
        ax.set_ylabel("Model nucleotide preference\n(softmax of weight table)")
        ax.set_xlabel("Position relative to AUG start codon")
        ax.set_title(title, fontsize=12)
        ax.legend(title="Nucleotide", fontsize=9, loc="upper left")

    stacked(axD, KD, "Dicot Kozak Context\n(A-rich; Joshi 1987)")
    stacked(axM, KM, "Monocot Kozak Context\n(GC-richer; Sawant 2001)")
    fig.tight_layout(rect=[0, 0, 1, 0.93])
    _panel(axD, "A"); _panel(axM, "B")
    _save(fig, "fig4_kozak.png")


# ── Figure 5 β€” expression cassette ───────────────────────────────────────────
def fig5_cassette():
    fig, ax = plt.subplots(figsize=(13.8, 6.0))
    ax.set_xlim(0, 100); ax.set_ylim(0, 100); ax.axis("off")
    _title(fig, 5, "Full Expression Cassette Architecture")
    ax.text(50, 86, "Validated Part Libraries", ha="center", fontsize=13, fontweight="bold")
    # Order mirrors the real cassette designer (cassette.py): promoter β†’
    # 5'UTR leader β†’ IME intron β†’ CDS β†’ terminator. The terminator carries the
    # 3'UTR + poly-A signal; the designer emits no separate 3'UTR element.
    modules = [("Promoter", "CaMV 35S / Ubi1 / Act1", BLUE, "~800 bp", 18),
               ("5β€² Leader", "TMV Ξ© / AMV", PURPLE, "~67 bp", 12),
               ("IME Intron", "Splicing enhancer", "#0e8a8a", "~200 bp", 15),
               ("Codon-Optimised CDS", "GA-designed sequence", DGREEN, "Variable", 24),
               ("Terminator", "NOS / rbcS-E9 (3β€²UTR + poly-A)", RED, "~250 bp", 22)]
    x = 2
    for name, sub, col, size, w in modules:
        ax.add_patch(FancyBboxPatch((x, 52), w, 18, boxstyle="round,pad=0.3,rounding_size=1.5",
                                    fc=col, ec="white", lw=1.5))
        ax.text(x + w / 2, 64, name, ha="center", va="center", color="white",
                fontsize=11, fontweight="bold")
        ax.text(x + w / 2, 57, sub, ha="center", va="center", color="white",
                fontsize=8.5, style="italic")
        ax.annotate("", (x, 47), (x + w, 47), arrowprops=dict(arrowstyle="<->", color="#555"))
        ax.text(x + w / 2, 43, size, ha="center", fontsize=9)
        x += w + 1
    legend = [(BLUE, "Strong constitutive promoter (species-selected from validated library)"),
              (PURPLE, "5β€² translational enhancer leader (verbatim validated sequence)"),
              ("#0e8a8a", "IME intron (clade-specific: AU-rich dicot / GC-balanced monocot)"),
              (DGREEN, "Codon-optimised CDS (de-novo, GA-designed, plant-specific)"),
              (RED, "Terminator (selected from validated library; supplies 3β€²UTR + poly-A)")]
    ax.text(3, 33, "Legend:", fontsize=11, fontweight="bold")
    for i, (col, txt) in enumerate(legend):
        yy = 27 - i * 6
        ax.add_patch(FancyBboxPatch((4, yy), 6, 3.5, boxstyle="round,pad=0.2,rounding_size=1",
                                    fc=col, ec="white"))
        ax.text(12, yy + 1.7, txt, fontsize=9.5, va="center")
    _save(fig, "fig5_cassette.png")


# ── Figure 6 β€” benchmark (7 strategies) ──────────────────────────────────────
def fig6_benchmark():
    # Labels identical to Table 2 for consistency (full tool mapping is in the caption).
    STR = ["Random", "CAI-max", "IDT", "TISIGNER", "CAI+GC", "PDD", "PDD-fold"]
    # Real-codon-table benchmark (GA 60Γ—80, rice, 6 effectors, TARGET_GC=50, seed=0);
    # tGCN from real GtRNAdb tables + dos Reis wobble-weighted tAI. Verbatim
    # `python -m benchmark` output (full tAI/MTDR columns are in Table 2).
    CAI = [0.731, 1.000, 0.793, 0.955, 1.000, 0.790, 0.775]
    HARM = [0.747, 0.422, 0.930, 0.495, 0.433, 0.774, 0.815]
    DYN = [0.722, 0.649, 0.740, 0.699, 0.650, 0.741, 0.761]
    OPENR = [-0.248, -0.347, -0.139, -0.153, -0.351, -0.114, -0.129]
    SAFE = [3.47, 2.33, 3.00, 2.92, 2.58, 0.00, 0.25]
    GCD = [3.5, 21.8, 7.5, 18.9, 21.0, 4.0, 4.1]
    OPENN = [1 + p for p in OPENR]
    SAFEN = [max(0, 1 - v / 6.0) for v in SAFE]
    # Per-strategy SD across the 6 proteins (from `python -m benchmark`), shown as
    # error bars. OPENN inherits 5β€²-open SD (linear shift); SAFEN scales SD by 1/6.
    CAI_SD = [0.008, 0.000, 0.005, 0.008, 0.000, 0.019, 0.008]
    HARM_SD = [0.021, 0.024, 0.007, 0.022, 0.018, 0.028, 0.019]
    DYN_SD = [0.015, 0.013, 0.016, 0.016, 0.013, 0.021, 0.026]
    OPEN_SD = [0.095, 0.161, 0.124, 0.103, 0.116, 0.059, 0.065]
    SAFE_SD = [0.876, 0.753, 1.342, 1.320, 0.861, 0.000, 0.612]
    SAFEN_SD = [s / 6.0 for s in SAFE_SD]
    METRICS = [("CAI", CAI, CAI_SD, BLUE), ("Harmony", HARM, HARM_SD, GREEN),
               ("Dyn Score", DYN, DYN_SD, ORANGE), ("5β€² Openness", OPENN, OPEN_SD, PURPLE),
               ("Safety (inv.)", SAFEN, SAFEN_SD, RED)]

    fig, (axL, axR) = plt.subplots(1, 2, figsize=(14.4, 5.8))
    _title(fig, 6, "Strategy Benchmark Results")
    n = len(STR); x = np.arange(n); w = 0.16
    for i, (lab, vals, sds, col) in enumerate(METRICS):
        axL.bar(x + (i - 2) * w, vals, w, label=lab, color=col, edgecolor="white", lw=0.4,
                yerr=sds, error_kw=dict(elinewidth=0.7, capsize=1.5, ecolor="0.35"))
    axL.set_title("7 Strategies Γ— 5 Metrics\n(comparators reproduce external-tool algorithms; 6 rice effectors)",
                  fontsize=12)
    axL.set_ylabel("Normalised Score (0–1)"); axL.set_xticks(x)
    axL.set_xticklabels(STR, fontsize=8.5); axL.set_ylim(0, 1.08)
    axL.legend(ncol=3, fontsize=8.5, loc="upper left", framealpha=0.9)
    axL.axvspan(4.5, 6.5, color="#dfe7dd", alpha=0.35, zorder=0)
    axL.grid(axis="y", color="white"); axL.set_axisbelow(True)

    axR2 = axR.twinx(); bw = 0.38
    b1 = axR.bar(x - bw / 2, GCD, bw, color="#4878a8", edgecolor="white", label="GC deviation (pp)")
    b2 = axR2.bar(x + bw / 2, SAFE, bw, color="#c44", edgecolor="white", hatch="///",
                  alpha=0.55, label="Safety violations")
    axR.set_title("GC Target Deviation & Safety Violations", fontsize=12)
    axR.set_ylabel("GC Deviation (pp)", color="#2c5d8f")
    axR2.set_ylabel("Transgene Safety Violations", color="#b22")
    axR.set_xticks(x); axR.set_xticklabels(STR, fontsize=8.5)
    axR.tick_params(axis="y", labelcolor="#2c5d8f"); axR2.tick_params(axis="y", labelcolor="#b22")
    axR.set_ylim(0, 25.5); axR2.set_ylim(0, 4.4)
    for xi, g in zip(x, GCD):
        axR.text(xi - bw / 2, g + 0.3, f"{g:.1f}", ha="center", fontsize=8, color="#2c5d8f")
    for xi, s in zip(x, SAFE):
        axR2.text(xi + bw / 2, s + 0.06, f"{s:.2f}", ha="center", fontsize=8, color="#b22")
    axR.legend([b1, b2], [b1.get_label(), b2.get_label()], loc="upper right", fontsize=9)
    axR.grid(axis="y", color="white"); axR.set_axisbelow(True)
    fig.tight_layout(rect=[0, 0, 1, 0.93])
    _panel(axL, "A"); _panel(axR, "B")
    _save(fig, "fig6_benchmark.png")


# ── Figure 7 β€” GA convergence + Pareto ───────────────────────────────────────
def fig7_ga_pareto():
    # REAL data: an actual GA convergence trace and an actual NSGA-II Pareto
    # front for a rice GRF4 design, read from the designer's own run reports.
    import random
    from core.mechanisms import EFFECTORS
    from core.designer import AdvancedDnaDesigner
    random.seed(0); np.random.seed(0)
    protein = EFFECTORS["GRF4"]["protein"]

    dc = AdvancedDnaDesigner(protein, "rice", population_size=80, generations=140,
                             codon_table="rice")
    dc.generate_sequence(45.0, [], ["AAAAAA"], mutation_rate=0.12)
    conv = dc.last_run["convergence"]
    g = np.array([c["generation"] for c in conv])
    best = np.array([c["best"] for c in conv])
    mean = np.array([c["mean"] for c in conv])
    gens_run = dc.last_run["generations_run"]

    fig, (axL, axR) = plt.subplots(1, 2, figsize=(13.5, 5.4))
    _title(fig, 7, "GA Convergence & Pareto Trade-Off Front")
    axL.plot(g, best, color=DBLUE, lw=2, label="Best fitness")
    axL.plot(g, mean, color=ORANGE, lw=1.6, label="Mean fitness")
    axL.set_xlabel("Generation"); axL.set_ylabel("Aggregate fitness score")
    axL.set_title(f"Genetic Algorithm Convergence\n(GRF4, rice; pop 80, {gens_run} generations run)",
                  fontsize=12)
    axL.legend(fontsize=8.5, loc="lower right"); axL.grid(color="white"); axL.set_axisbelow(True)

    dp = AdvancedDnaDesigner(protein, "rice", population_size=60, generations=60,
                             codon_table="rice")
    front = dp.generate_pareto(45.0, [], ["AAAAAA"], front_size=40)
    expr = np.array([f["axes"]["expression"] for f in front])
    stab = np.array([f["axes"]["stability"] for f in front])
    safe = np.array([f["axes"]["safety"] for f in front])
    sc = axR.scatter(expr, stab, c=safe, cmap="RdYlGn", s=55, edgecolor="#444", lw=0.5)
    axR.scatter([expr[0]], [stab[0]], marker="*", s=340, color=GREEN, edgecolor="k",
                zorder=6, label="Balanced knee")
    fig.colorbar(sc, ax=axR, label="Safety axis (higher = cleaner)", fraction=0.046, pad=0.02)
    axR.set_xlabel("Expression axis"); axR.set_ylabel("mRNA stability axis")
    axR.set_title(f"NSGA-II Pareto Front ({len(front)} designs)\n(expression vs. stability, coloured by safety)",
                  fontsize=12)
    axR.legend(fontsize=8.5, loc="best"); axR.grid(color="white"); axR.set_axisbelow(True)
    fig.tight_layout(rect=[0, 0, 1, 0.93])
    _panel(axL, "A"); _panel(axR, "B")
    _save(fig, "fig7_ga_pareto.png")


# ── Figure 8 β€” pathway balance + 18-species coverage ─────────────────────────
def fig8_pathway_species():
    sys.path.insert(0, os.path.join(HERE, "..", "cool"))
    from core.species import SPECIES_PROFILES, clade_for

    fig, (axL, axR) = plt.subplots(1, 2, figsize=(14.6, 6.4),
                                   gridspec_kw={"width_ratios": [1, 1.25]})
    _title(fig, 8, "Pathway Balance & 18-Species Coverage Matrix")

    # REAL pathway design: a rice biofortification co-expression stack built from
    # effectors in the repo, run through the actual multi-gene designer.
    from core.mechanisms import EFFECTORS
    from core.pathway import design_pathway
    stack = [("Ferritin", "Fe", 1.0), ("OsNAS2", "Zn", 0.8),
             ("PSY", "provit-A", 0.6), ("GTPCHI", "folate", 0.4)]
    pgenes = [{"name": n, "trait": t, "protein": EFFECTORS[n]["protein"], "level": L}
              for n, t, L in stack]
    res = design_pathway(pgenes, clade="monocot", codon_table="rice")
    short = {n: t for n, t, _ in stack}
    genes = [f"{g['name']}\n({short[g['name']]})" for g in res["genes"]]
    levels = [g["target_level"] for g in res["genes"]]
    tcai = [g["target_cai"] for g in res["genes"]]
    pred = [g["predicted_expression"] for g in res["genes"]]
    x = np.arange(len(genes)); w = 0.27
    axL.bar(x - w, tcai, w, label="Target CAI", color=BLUE, edgecolor="white")
    axL.bar(x, pred, w, label="Predicted expression", color=GREEN, edgecolor="white")
    axL.bar(x + w, levels, w, label="Relative level target", color=ORANGE, edgecolor="white", hatch="//")
    for xi, t in zip(x, tcai):
        axL.text(xi - w, t + 0.01, f"{t:.2f}", ha="center", fontsize=7.5)
    axL.set_xticks(x); axL.set_xticklabels(genes, fontsize=8.5)
    axL.set_ylim(0, 1.1); axL.set_ylabel("Score / Level (0–1)")
    axL.set_title("Multi-Gene Pathway Balance\n(rice biofortification stack)", fontsize=12)
    axL.legend(fontsize=8.5, loc="upper right")
    axL.text(-0.38, 1.05, f"Balance score: {res['balance_score']:.2f} ({res['balance_grade']})",
             ha="left", fontsize=9.5, color=DGREEN, fontweight="bold",
             bbox=dict(boxstyle="round", fc="#dff0df", ec=GREEN))
    axL.grid(axis="y", color="white"); axL.set_axisbelow(True)

    # coverage matrix β€” read live from the code/data so it never drifts
    order = ["rice", "maize", "arabidopsis", "tomato", "soybean", "wheat", "barley",
             "sorghum", "potato", "cassava", "tobacco", "grape", "cotton", "sugarcane",
             "canola", "banana", "peanut", "sunflower"]
    # Crops whose OWN sequenced genome is committed as real GtRNAdb tGCN data
    # (core/data/tgcn/<crop>.fa). Read from disk so adding a genome auto-upgrades
    # the matrix; the rest map to a real nearest relative (proxy).
    tgcn_dir = os.path.join(HERE, "..", "cool", "core", "data", "tgcn")
    own_tgcn = {os.path.splitext(f)[0].lower() for f in os.listdir(tgcn_dir)
                if f.endswith(".fa")}
    feats = ["Codon\nTable", "tRNA\ntGCN", "Kozak\nContext", "IME\nIntron", "miRNA\nLibrary"]
    M = np.zeros((len(order), len(feats)))
    for r, sp in enumerate(order):
        M[r, 0] = 1.0                       # codon table β€” REAL per-species CUTG for all 18
        M[r, 1] = 1.0 if sp in own_tgcn else 0.5  # tGCN β€” own GtRNAdb genome vs nearest-real relative
        M[r, 2] = 1.0                       # Kozak β€” clade-specific, all covered
        M[r, 3] = 1.0                       # IME intron β€” clade-specific
        M[r, 4] = 1.0                       # miRNA β€” base library + monocot extras
    cmap = matplotlib.colors.LinearSegmentedColormap.from_list("cov", ["#ffffff", "#e8a13a", "#1d6b2e"])
    axR.imshow(M, cmap=cmap, vmin=0, vmax=1, aspect="auto")
    axR.set_xticks(range(len(feats))); axR.set_xticklabels(feats, fontsize=9)
    names = [SPECIES_PROFILES[s]["common_name"] for s in order]
    axR.set_yticks(range(len(order)))
    axR.set_yticklabels([f"{n}" for n in names], fontsize=8)
    for r, sp in enumerate(order):
        for c in range(len(feats)):
            v = M[r, c]
            mark = "●" if v == 1.0 else ("β—‘" if v == 0.5 else "β—‹")
            axR.text(c, r, mark, ha="center", va="center",
                     color="white" if v == 1.0 else "#333", fontsize=10)
        # clade tag
        tag = "M" if clade_for(sp) == "monocot" else "D"
        axR.text(len(feats) - 0.35, r, tag, ha="left", va="center", fontsize=7,
                 color="#666", fontweight="bold")
    axR.set_title("Species Feature Coverage\n(● full  β—‘ proxy  β—‹ none;  M=monocot D=dicot)",
                  fontsize=12)
    fig.tight_layout(rect=[0, 0, 1, 0.93])
    _panel(axL, "A"); _panel(axR, "B")
    _save(fig, "fig8_pathway_species.png")


def main():
    fig1_architecture()
    fig2_fitness()
    fig3_dynamics()
    fig4_kozak()
    fig5_cassette()
    fig6_benchmark()
    fig7_ga_pareto()
    fig8_pathway_species()
    print("\nAll 8 figures regenerated with correct, sequential numbering.")


if __name__ == "__main__":
    main()