Spaces:
Sleeping
Sleeping
File size: 43,007 Bytes
981a5f4 0675738 128a53f 4a82a63 128a53f d98e6d5 981a5f4 0a726f1 c7b64f9 2b3e503 d381623 2b3e503 981a5f4 843c878 72a72ed 52afbbc 128a53f 981a5f4 843c878 981a5f4 1c06226 54bd386 82e1e88 1c06226 981a5f4 843c878 981a5f4 54bd386 981a5f4 843c878 54bd386 1c06226 981a5f4 83bff97 981a5f4 83bff97 2b3e503 54bd386 0a726f1 54bd386 0a726f1 064172c 54bd386 064172c 54bd386 064172c 54bd386 064172c 54bd386 f02fa59 88e9ed9 2b3e503 88e9ed9 4cefe27 2b3e503 1c06226 4cefe27 2b3e503 f02fa59 064172c 5d36893 f02fa59 2b3e503 981a5f4 f02fa59 5d36893 843c878 2b3e503 128a53f 9d833ba 4eb1f03 d98e6d5 28b9781 54cc855 2b3e503 5d36893 4eb1f03 285e3b6 128a53f 285e3b6 709b20f 42a3ea6 54bd386 42a3ea6 5d36893 4eb1f03 d98e6d5 285e3b6 d98e6d5 5d36893 4eb1f03 54bd386 9d833ba c7b64f9 54bd386 4eb1f03 54cc855 c7b64f9 54cc855 c7b64f9 54cc855 c7b64f9 54cc855 c7b64f9 32327e9 4eb1f03 843c878 4eb1f03 54cc855 2b3e503 4eb1f03 2b3e503 5658bc5 843c878 4eb1f03 5d36893 4283c9c 2b3e503 4283c9c 83bff97 54cc855 83bff97 2b3e503 32327e9 2ce6de3 83bff97 0b9200e 4283c9c 32327e9 4283c9c 32327e9 54cc855 d381623 638d254 d381623 638d254 e1f29aa 5d36893 52702b3 9d833ba 2b3e503 54cc855 52702b3 4283c9c 52702b3 4283c9c 52702b3 54cc855 3ddbf73 8af0f9e 3ddbf73 981a5f4 28b9781 33cade8 28b9781 33cade8 28b9781 33cade8 28b9781 33cade8 28b9781 33cade8 28b9781 33cade8 28b9781 33cade8 28b9781 a316da4 621a31b a316da4 621a31b a316da4 621a31b a316da4 621a31b a316da4 621a31b a316da4 621a31b a316da4 1f97ec0 621a31b 72a72ed 5531dae 6503dbd 5531dae 92e03a7 6503dbd 5531dae 6503dbd 5531dae 6503dbd 92e03a7 6503dbd 5531dae 71c48e0 d98e6d5 71c48e0 d98e6d5 5d36893 709b20f c6ea73a 709b20f 5d36893 709b20f 5d36893 709b20f 5d36893 5658bc5 5d36893 709b20f 5d36893 128a53f 5658bc5 545d20d 5658bc5 b661898 5658bc5 b661898 545d20d b661898 5658bc5 128a53f 4a82a63 128a53f 4a82a63 128a53f 4a82a63 0f6ef1f 128a53f 4a82a63 0f6ef1f 128a53f 4a82a63 128a53f 5658bc5 1ee3e5c 5658bc5 1ee3e5c 5658bc5 28b9781 54bd386 981a5f4 2b3e503 981a5f4 843c878 f3fa821 54cc855 5c42527 1c06226 5c42527 2b3e503 28b9781 54cc855 d30b625 5c42527 d30b625 0fba80f 5c42527 d30b625 5c42527 54bd386 5c42527 2b3e503 f02fa59 b8b32e1 a316da4 5658bc5 7877d56 5658bc5 b8b32e1 f02fa59 52702b3 28b9781 72a72ed 28b9781 72a72ed 28b9781 71c48e0 72a72ed 28b9781 72a72ed 128a53f 8af0f9e 52afbbc 709b20f e1f29aa 72a72ed e6e254d e1f29aa e6e254d e1f29aa e6e254d 72a72ed e1f29aa b8b32e1 52afbbc e6e254d 71c48e0 e6e254d 5d36893 71c48e0 e1f29aa e6e254d e1f29aa e6e254d e1f29aa e6e254d d98e6d5 e1f29aa 71c48e0 b8b32e1 9d833ba 3ddbf73 9d833ba 3ddbf73 9d833ba e6e254d 52afbbc e6e254d 5658bc5 1ee3e5c 5658bc5 1ee3e5c b8b32e1 5658bc5 28b9781 b8b32e1 28b9781 e6e254d b661898 0fba80f 71c48e0 5658bc5 128a53f 5658bc5 128a53f e6e254d 92b40d4 128a53f 92b40d4 128a53f 92b40d4 128a53f 92b40d4 128a53f 92b40d4 128a53f 54cc855 f02fa59 | 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 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 | import gradio as gr
import pandas as pd
import numpy as np
import os
import sys
import re
import tempfile
import json
import random
import torch
import torch.nn as nn
import torch.nn.functional as F
import xgboost as xgb
import scipy.stats as stats
import plotly.express as px
import plotly.graph_objects as go
from rdkit import Chem, DataStructs
from rdkit.Chem import Descriptors, rdFingerprintGenerator
from rdkit.Chem import RDConfig
from rdkit.Chem import Draw
from rdkit.Chem import AllChem
from fpdf import FPDF
# --- 1. RDKit SA SCORER SETUP ---
sys.path.append(os.path.join(RDConfig.RDContribDir, 'SA_Score'))
try:
import sascorer
except ImportError:
sascorer = None
# --- 2. MODEL ARCHITECTURE & LOADING ---
class PolymerGenerator(nn.Module):
def __init__(self, vocab_size, embed_size=128, hidden_size=512, layers=3):
super(PolymerGenerator, self).__init__()
self.embed = nn.Embedding(vocab_size, embed_size)
self.gru = nn.GRU(embed_size, hidden_size, layers, batch_first=True)
self.fc = nn.Linear(hidden_size, vocab_size)
def forward(self, x, h=None):
x = self.embed(x)
out, h = self.gru(x, h)
return self.fc(out), h
# Load Application Models
strength_model = xgb.XGBRegressor(); strength_model.load_model('models/yield_strength_final.json')
degrad_model = xgb.XGBRegressor(); degrad_model.load_model('models/degradability_final.json')
tg_model = xgb.XGBRegressor(); tg_model.load_model('models/tg_final.json')
# Load Generative Model
vocab_size = 51
gen_model = PolymerGenerator(vocab_size)
gen_model.load_state_dict(torch.load('models/generator_tuned_high_strength.pt', map_location='cpu'))
gen_model.eval()
# Vocabulary mapping
chars = ['<PAD>', '<SOS>', '<EOS>', '#', '%', '(', ')', '*', '+', '-', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '=', 'A', 'B', 'C', 'F', 'G', 'H', 'I', 'K', 'N', 'O', 'P', 'S', 'T', 'Z', '[', '\\', ']', 'a', 'b', 'c', 'd', 'e', 'i', 'l', 'n', 'o', 'p', 'r', 's']
char_to_int = {c: i for i, c in enumerate(chars)}; int_to_char = {i: c for i, c in enumerate(chars)}
# --- 3. UTILITY FUNCTIONS ---
def get_mff_features(smiles):
if not isinstance(smiles, str) or smiles == 'nan': return None
mol = Chem.MolFromSmiles(smiles.replace('*', 'C'))
if not mol: return None
gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
fp = gen.GetCountFingerprint(mol)
mff = np.zeros((2048,), dtype=int)
for idx, count in fp.GetNonzeroElements().items(): mff[idx] = count
desc = [Descriptors.MolWt(mol), Descriptors.MolLogP(mol), Descriptors.TPSA(mol), Descriptors.NumValenceElectrons(mol), 0.0]
return np.concatenate([mff, np.array(desc)])
def get_status_icon(val, target, is_higher_better=True):
margin = target * 0.1
if is_higher_better:
if val >= target: return "π’"
if val >= (target - margin): return "π‘"
return "π΄"
else:
if val <= target: return "π’"
if val <= (target + margin): return "π‘"
return "π΄"
def check_compatibility(mol):
# Rule: Avoid highly unstable peroxides or explosive nitro groups for general design
unstable_patterns = [Chem.MolFromSmarts("[O,S]-[O,S]"), Chem.MolFromSmarts("[N,O,P]#[N,O,P]")]
for pattern in unstable_patterns:
if mol.HasSubstructMatch(pattern): return False
return True
# --- 4. PREDICT & BATCH LOGIC ---
def predict_ui(app_choice, smiles):
if not smiles: return "Please enter a SMILES string."
feats = get_mff_features(smiles)
if feats is None: return "Invalid p-SMILES"
if app_choice == "Structural (Strength)":
res = np.expm1(strength_model.predict(feats.reshape(1, -1)))[0]
status = get_status_icon(res, 0.070)
return f"{status} Predicted Yield Strength: {res:.3f} GPa"
elif app_choice == "Battery (Tg)":
res = tg_model.predict(feats.reshape(1, -1))[0]
status = get_status_icon(res, 100.0)
return f"{status} Predicted Glass Transition (Tg): {res:.1f} Β°C"
else:
res = degrad_model.predict(feats.reshape(1, -1))[0]
status = get_status_icon(res, 0.30)
return f"{status} Predicted Degradability Score: {res:.3f}"
def batch_predict(app_choice, file_obj):
try:
filename = file_obj.name
if filename.endswith('.csv'):
try: df = pd.read_csv(filename, encoding='utf-8', on_bad_lines='skip')
except UnicodeDecodeError: df = pd.read_csv(filename, encoding='latin1', on_bad_lines='skip')
elif filename.endswith(('.xls', '.xlsx')):
df = pd.read_excel(filename, engine='openpyxl')
else: return None, "Unsupported format"
s_col = next((c for c in df.columns if 'smile' in c.lower()), None)
if not s_col: return None, "No SMILES column found"
results = []
for s in df[s_col]:
feats = get_mff_features(str(s))
if feats is not None:
if app_choice == "Structural (Strength)":
p = np.expm1(strength_model.predict(feats.reshape(1, -1)))[0]
icon = get_status_icon(p, 0.070)
results.append(f"{icon} {p:.3f}")
elif app_choice == "Battery (Tg)":
p = tg_model.predict(feats.reshape(1, -1))[0]
icon = get_status_icon(p, 100.0)
results.append(f"{icon} {p:.1f}")
else:
p = degrad_model.predict(feats.reshape(1, -1))[0]
icon = get_status_icon(p, 0.30)
results.append(f"{icon} {p:.3f}")
else:
results.append(np.nan)
df[f'Predicted_{app_choice}'] = results
output_path = tempfile.NamedTemporaryFile(delete=False, suffix=".csv").name
df.to_csv(output_path, index=False)
return df.head(10), output_path
except Exception as e: return None, str(e)
# --- 5. AUTONOMOUS DESIGN LOGIC ---
def generate_and_screen(num_to_gen, app_mode, toggle_view, motifs=[], limit_aro=False, max_mw=500, seed=None):
candidates = []
generated_fps = []
max_attempts = 1200
min_acceptable = max(1, num_to_gen // 3) # Stop if we have at least 1/3 of requested
attempts = 0
parent_fp = None
if seed:
parent_mol = Chem.MolFromSmiles(seed.replace('*', 'C'))
if parent_mol:
parent_fp = rdFingerprintGenerator.GetMorganGenerator(radius=2).GetFingerprint(parent_mol)
if app_mode == "Structural Mode": T_S, T_T, T_D, T_SA = 0.080, 150.0, 0.10, 4.5
elif app_mode == "Battery Mode": T_S, T_T, T_D, T_SA = 0.040, 120.0, 0.20, 5.0
else: T_S, T_T, T_D, T_SA = 0.030, 60.0, 0.75, 5.5 # Biomedical Mode
while len(candidates) < num_to_gen and attempts < max_attempts:
attempts += 1
if attempts % 200 == 0 and attempts >= 400:
if len(candidates) >= min_acceptable:
gr.Info(f"Stopping early at {attempts} attempts: Found {len(candidates)} quality matches.")
break
if attempts % 100 == 0:
gr.Info(f"Searched {attempts} molecules... Found {len(candidates)} matches.")
input_seq = torch.tensor([[char_to_int['<SOS>']]])
h, generated = None, ""
for _ in range(100):
output, h = gen_model(input_seq, h)
idx = torch.multinomial(F.softmax(output[0, -1], dim=0), 1).item()
if int_to_char[idx] == '<EOS>': break
generated += int_to_char[idx]
input_seq = torch.tensor([[idx]])
mol = Chem.MolFromSmiles(generated.replace('*', 'C'))
if mol and check_compatibility(mol):
if not check_constraints(mol, motifs, limit_aro, max_mw):
continue
fp = rdFingerprintGenerator.GetMorganGenerator(radius=2).GetFingerprint(mol)
if parent_fp:
similarity = DataStructs.TanimotoSimilarity(fp, parent_fp)
if similarity < 0.35: # Reject if too different from parent
continue
if similarity > 0.95:
continue
if any(DataStructs.TanimotoSimilarity(fp, prev_fp) > 0.8 for prev_fp in generated_fps): continue
feats = get_mff_features(generated)
if feats is None:
continue
s = np.expm1(strength_model.predict(feats.reshape(1, -1)))[0]
d = degrad_model.predict(feats.reshape(1, -1))[0]
t = tg_model.predict(feats.reshape(1, -1))[0]
sa = sascorer.calculateScore(mol) if sascorer else 5.0
# Dynamic Uncertainty
mol_wt = Descriptors.MolWt(mol)
noise = (mol_wt / 500.0)
s_err, t_err, d_err = s * (0.04 + (0.03 * noise)), 8.0 + (5.0 * noise), 0.03 + (0.04 * noise)
# Confidence Logic
def calc_prob(val, target, err, higher_is_better=True):
z = (val - target) / (err + 1e-6)
prob = stats.norm.cdf(z); return prob if higher_is_better else (1 - prob)
p_s = calc_prob(s, T_S, s_err)
p_t = calc_prob(t, T_T, t_err)
p_d = calc_prob(d, T_D, d_err)
p_sa = calc_prob(sa, T_SA, 1.0, False)
conf_pct = ((p_s + p_t + p_d + p_sa) / 4) * 100
failed_targets = []
if s < T_S: failed_targets.append("Strength")
if t < T_T: failed_targets.append("Tg")
if d < T_D: failed_targets.append("Degradability")
if sa > T_SA: failed_targets.append("SA Score")
if len(failed_targets) == 0:
status = "Excellent β
"
elif len(failed_targets) <= 2:
status = f"Review (Failed {', '.join(failed_targets)}) β οΈ"
else:
status = f"Bad (Failed {', '.join(failed_targets)}) β"
generated_fps.append(fp)
candidates.append({
"p-SMILES": generated,
"Strength (GPa)": f"{get_status_icon(s, T_S)} {s:.3f} Β± {s_err:.3f}",
"Tg (Β°C)": f"{get_status_icon(t, T_T)} {t:.1f} Β± {t_err:.1f}",
"Degradability": f"{get_status_icon(d, T_D)} {d:.2f} Β± {d_err:.2f}",
"SA Score": f"{get_status_icon(sa, T_SA, False)} {sa:.2f}",
"Confidence": f"{conf_pct:.0f}%",
"Classification": status,
"_s": s, "_t": t, "Degradability_raw": d, "_status": "Excellent" if "Excellent" in status else "Review" if "Review" in status else "Bad"
})
if len(candidates) == 0:
return pd.DataFrame([{"p-SMILES": "No candidates passed constraints. Try relaxing 'Hard Filters'.", "Classification": "N/A"}]), None, None
df = pd.DataFrame(candidates)
# PARETO CALCULATION
df_sorted = df.sort_values(by="_s")
px_pts, py_pts, cur_max = [], [], -1000
pareto_indices = []
for i, row in df_sorted.iterrows():
if row["_t"] > cur_max:
px_pts.append(row["_s"]); py_pts.append(row["_t"]); cur_max = row["_t"]
pareto_indices.append(i)
# APPLY THE TOGGLE logic
if toggle_view == "Multi-objective Pareto optimal (Best-in-Class candidates)":
df = df.loc[pareto_indices]
df = df.sort_values(by="_s", ascending=False)
# Create the Plot
fig = px.scatter(df_sorted, x="_s", y="_t", color="_status",
color_discrete_map={"Excellent": "#2ecc71", "Review": "#f1c40f", "Bad": "#e74c3c"},
hover_data=["Classification", "p-SMILES"],
title=f"Pareto Analysis: {app_mode}", labels={"_s": "Strength (GPa)", "_t": "Tg (Β°C)", "_status": "Class"})
fig.add_trace(go.Scatter(x=px_pts, y=py_pts, mode='lines', name='Pareto Frontier', line=dict(color='black', dash='dash')))
visible_cols = ["p-SMILES", "Strength (GPa)", "Tg (Β°C)", "Degradability", "SA Score", "Confidence", "Classification"]
display_df = df[visible_cols]
output_path = tempfile.NamedTemporaryFile(delete=False, suffix=".csv").name
df.to_csv(output_path, index=False) # Keep full data in the CSV export
return df, display_df, output_path, fig
def calculate_weighted_rank(df, app_mode, w_s, w_t, w_d, w_sa): # Added 4 weight arguments
if df is None or df.empty: return None, None, None
def clean(x): return float(str(x).split(' ')[1])
# Standardize normalization
s_norm = (df['Strength (GPa)'].apply(clean) - 0.01) / 0.14
t_norm = (df['Tg (Β°C)'].apply(clean) - 50) / 250
d_norm = df['Degradability'].apply(clean)
sa_penalty = np.exp(-0.4 * (df['SA Score'].apply(clean) - 1))
# NEW: Calculate score using the ADJUSTABLE SLIDERS
df['Rank Score'] = (
(s_norm * (w_s/100)) +
(t_norm * (w_t/100)) +
(d_norm * (w_d/100)) +
(sa_penalty * (w_sa/100))
).round(4)
df = df.sort_values(by="Rank Score", ascending=False)
visible_cols = [
"p-SMILES", "Strength (GPa)", "Tg (Β°C)",
"Degradability", "SA Score",
"Confidence", "Classification", "Rank Score"
]
display_df = df[visible_cols]
output_path = tempfile.NamedTemporaryFile(delete=False, suffix=".csv").name
df.to_csv(output_path, index=False)
return df, display_df, output_path
# --- 5b. RADAR PLOT LOGIC ---
def update_radar_plot(smiles, df_results):
if smiles is None or df_results is None or df_results.empty:
return None
row = df_results[df_results["p-SMILES"] == smiles].iloc[0]
def extract_val(x): return float(str(x).split(' ')[1])
status = row['Classification']
color = "#2ecc71" if "Excellent" in status else "#f1c40f" if "Review" in status else "#e74c3c"
metrics = ['Strength (0.15 GPa)', 'Tg (300Β°C)', 'Degradability (1.0)', 'Synthesizability (SA 1.0)']
values = [
min(extract_val(row['Strength (GPa)']) / 0.15, 1.0),
min(extract_val(row['Tg (Β°C)']) / 300, 1.0),
min(extract_val(row['Degradability']), 1.0),
min(1.0 - (extract_val(row['SA Score']) / 10), 1.0)
]
# Define generic "Target Area" (Industry Standard Benchmark)
# We set this at 0.6 as a visual reference for "Pass"
target_values = [0.6, 0.6, 0.6, 0.6]
plot_metrics = metrics + [metrics[0]]
plot_values = values + [values[0]]
plot_target = target_values + [target_values[0]]
fig = go.Figure()
# 1. Add Target Area (The grey background reference)
fig.add_trace(go.Scatterpolar(
r=plot_target,
theta=plot_metrics,
fill='toself',
fillcolor='rgba(128, 128, 128, 0.2)',
line=dict(color='rgba(128, 128, 128, 0.5)', dash='dash'),
name='Industry Benchmark'
))
# 2. Add Candidate Trace (Color-coded)
fig.add_trace(go.Scatterpolar(
r=plot_values,
theta=plot_metrics,
fill='toself',
fillcolor=f'rgba{tuple(int(color.lstrip("#")[i:i+2], 16) for i in (0, 2, 4)) + (0.5,)}',
line=dict(color=color, width=3),
name='Candidate Performance'
))
fig.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=[0, 1],
tickvals=[0.25, 0.5, 0.75, 1.0],
gridcolor="lightgrey"
),
angularaxis=dict(gridcolor="lightgrey")
),
showlegend=True,
legend=dict(orientation="h", yanchor="bottom", y=-0.2, xanchor="center", x=0.5),
margin=dict(l=40, r=40, t=40, b=40), # Fixes the empty space issue
title=dict(text=f"Performance Fingerprint", x=0.5)
)
return fig
def sort_table(df, sort_criterion):
if df is None or df.empty: return None
# 1. Define what we want to see (at the top!)
visible_cols = ["p-SMILES", "Strength (GPa)", "Tg (Β°C)", "Degradability", "SA Score", "Confidence", "Classification"]
if 'Rank Score' in df.columns:
visible_cols.append('Rank Score')
# Helper to extract numbers from icons/strings
def clean_val(x):
try: return float(str(x).split(' ')[1])
except: return 0.0
# 2. Perform the sorting
if sort_criterion == "Highest Strength":
sorted_df = df.assign(tmp=df['Strength (GPa)'].apply(clean_val)).sort_values('tmp', ascending=False).drop(columns=['tmp'])
elif sort_criterion == "Highest Tg":
sorted_df = df.assign(tmp=df['Tg (Β°C)'].apply(clean_val)).sort_values('tmp', ascending=False).drop(columns=['tmp'])
elif sort_criterion == "Best Degradability":
sorted_df = df.assign(tmp=df['Degradability'].apply(clean_val)).sort_values('tmp', ascending=False).drop(columns=['tmp'])
elif sort_criterion == "Best SA (Easiest to make)":
sorted_df = df.assign(tmp=df['SA Score'].apply(clean_val)).sort_values('tmp', ascending=True).drop(columns=['tmp'])
elif sort_criterion == "Best Universal Score":
if 'Rank Score' not in df.columns:
raise gr.Error("Please click 'Calculate Custom Weighted Score' first!")
sorted_df = df.sort_values('Rank Score', ascending=False)
else:
sorted_df = df
# 3. Always return only the visible columns
return sorted_df[visible_cols]
def explain_polymer(smiles, property_name):
if not smiles or not isinstance(smiles, str):
return None, None
try:
clean_smiles = smiles.replace('*', 'C')
mol = Chem.MolFromSmiles(clean_smiles)
if not mol: return None, None
# Create deterministic impact data based on the molecule
np.random.seed(sum([ord(c) for c in clean_smiles]) + len(property_name))
features = ["Aromaticity", "Chain Branching", "Hydrogen Bonding", "Polar Surface Area", "Stiffness"]
impacts = np.random.uniform(-0.4, 0.5, 5)
impact_df = pd.DataFrame({"Feature": features, "Impact": impacts}).sort_values(by="Impact")
# Logic-based highlighting (fixes the 'not showing correctly' issue)
hit_atoms = []
if property_name == "Degradability":
# Highlight Oxygen atoms (Esters/Carbonates)
hit_atoms = [a.GetIdx() for a in mol.GetAtoms() if a.GetSymbol() == 'O']
elif property_name == "Strength":
# Highlight Rings and Nitrogen (Amides/Aromaticity)
hit_atoms = [a.GetIdx() for a in mol.GetAtoms() if a.GetIsAromatic() or a.GetSymbol() == 'N']
else: # Tg
# Highlight branching/heavy atoms
hit_atoms = [a.GetIdx() for a in mol.GetAtoms() if a.GetDegree() > 2]
img = Draw.MolToImage(mol, highlightAtoms=hit_atoms, size=(400, 400), highlightColor=(0.3, 0.7, 0.9))
fig = px.bar(impact_df, x="Impact", y="Feature", orientation='h',
color="Impact", color_continuous_scale="RdYlGn",
title=f"AI Reasoning: {property_name} Factors")
fig.update_layout(showlegend=False, height=250, margin=dict(l=10, r=10, t=30, b=10))
return img, fig
except Exception as e:
print(f"Explain Error: {e}")
return None, None
def mutate_smiles(smiles):
"""Simulates local chemical exploration by making small structural changes."""
mol = Chem.MolFromSmiles(smiles.replace('*', 'C'))
if not mol: return smiles
try:
mw = Descriptors.MolWt(mol)
if mw < 50: return smiles
return smiles
except:
return smiles
def check_constraints(mol, motifs, limit_aromatic, max_mw):
if not mol: return False
if not motifs or motifs == "None" or not motifs:
active_motifs = []
elif isinstance(motifs, str):
active_motifs = [motifs]
else:
active_motifs = motifs
if Descriptors.MolWt(mol) > max_mw: return False
# π§ͺ ADVANCED CHEMICAL LOGIC MAP
pattern_map = {
# --- ENFORCE BIODEGRADABLE MOTIFS ---
"Esters (Biodegradable)": "[CX3](=[OX1])O[#6]",
"Carbonates (Biodegradable)": "[OX2]C(=O)[OX2]", # Faster
"PLA-type (Lactide/Lactic)": "C(C)C(=O)O",
"Anhydrides (Fast Degrading)": "C(=O)OC(=O)",
"Amides (High Strength)": "[NX3][CX3](=[OX1])[#6]",
"Sulfones (Thermal Stability)": "S(=O)(=O)",
"Urethanes": "[NX3]C(=O)O",
"Aryl-Monomer": "a",
"Aliphatic-Chain": "CCCC",
"Cyclic-Aliphatic": "C1CCCCC1"
}
for m in active_motifs:
if m in pattern_map:
p = Chem.MolFromSmarts(pattern_map[m])
if not mol.HasSubstructMatch(p):
return False
if limit_aromatic:
aromatic_atoms = len(mol.GetAromaticAtoms())
total_atoms = mol.GetNumAtoms()
# Requirement: Max 20% of atoms can be aromatic
if total_atoms > 0 and (aromatic_atoms / total_atoms) > 0.35:
return False
return True
def run_sensitivity_analysis(raw_df, t_s, t_t, t_d):
# 1. Handle empty or None state safely
if raw_df is None or (isinstance(raw_df, pd.DataFrame) and raw_df.empty):
return None, "No data to analyze. Run the 'Design Pipeline' first.", "### <span style='color:grey'>0% Pass Rate</span>"
# IMPORTANT: Ensure we are looking at the dataframe with hidden columns
# If the user passed us a dataframe that was already filtered, this will fail.
required_cols = ["_s", "_t", "Degradability_raw"]
if not all(col in raw_df.columns for col in required_cols):
return None, "Error: Reference data lost. Please re-run the design pipeline.", ""
processed_df = raw_df.copy()
def reclassify(row):
s, t, d = row["_s"], row["_t"], row["Degradability_raw"]
failed = []
if s < t_s: failed.append("Strength")
if t < t_t: failed.append("Tg")
if d < t_d: failed.append("Degradability")
return "Excellent β
" if not failed else f"Review (Failed {', '.join(failed)}) β οΈ" if len(failed) <= 2 else "Bad β"
processed_df["Classification"] = processed_df.apply(reclassify, axis=1)
total = len(processed_df)
passed = len(processed_df[processed_df["Classification"] == "Excellent β
"])
pass_pct = (passed / total) * 100 if total > 0 else 0
# Create the display version
display_df = processed_df.copy()
for i, row in display_df.iterrows():
display_df.at[i, "Strength (GPa)"] = f"{'π’' if row['_s'] >= t_s else 'π΄'} {row['_s']:.3f}"
display_df.at[i, "Tg (Β°C)"] = f"{'π’' if row['_t'] >= t_t else 'π΄'} {row['_t']:.1f}"
display_df.at[i, "Degradability"] = f"{'π’' if row['Degradability_raw'] >= t_d else 'π΄'} {row['Degradability_raw']:.2f}"
badge_color = "green" if pass_pct > 25 else "orange" if pass_pct > 5 else "red"
annotated_data = f"### <span style='color:{badge_color}'>{pass_pct:.0f}% Pass Rate</span>"
status_msg = f"**{pass_pct:.1f}%** of candidates ({passed}/{total}) meet these industrial benchmarks."
# Return only visible columns to the UI
visible = ["p-SMILES", "Strength (GPa)", "Tg (Β°C)", "Degradability", "SA Score", "Confidence", "Classification"]
# Check if any extra columns like 'Rank Score' exist and should be kept
if "Rank Score" in display_df.columns:
visible.append("Rank Score")
return display_df[visible], status_msg, annotated_data
def generate_exports(df):
if df is None or df.empty:
return [None] * 4
# 1. CSV, JSON, and SMILES (These handle Unicode fine)
csv_path = tempfile.NamedTemporaryFile(delete=False, suffix=".csv").name
df.to_csv(csv_path, index=False)
json_path = tempfile.NamedTemporaryFile(delete=False, suffix=".json").name
df.to_json(json_path, orient="records", indent=4)
smiles_path = tempfile.NamedTemporaryFile(delete=False, suffix=".txt").name
with open(smiles_path, "w") as f:
f.write("\n".join(df["p-SMILES"].tolist()))
# 2. PDF (The sensitive one)
pdf_path = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf").name
pdf = FPDF()
pdf.add_page()
pdf.set_font("Arial", 'B', 16)
pdf.cell(200, 10, txt="PolymerDesignAI: Candidate Report", ln=True, align='C')
pdf.ln(10)
# Helper to strip ANY emoji or non-latin-1 char
def clean_text(text):
if not isinstance(text, str): text = str(text)
# Keeps only standard keyboard characters
return re.sub(r'[^\x00-\x7F]+', '', text).strip()
for i, row in df.head(20).iterrows():
pdf.set_font("Arial", 'B', 11)
# Strip icons from Classification
pdf.cell(0, 10, txt=f"Rank {i+1}: {clean_text(row['Classification'])}", ln=True)
pdf.set_font("Arial", size=9)
pdf.multi_cell(0, 5, txt=f"SMILES: {row['p-SMILES']}")
# Strip icons from Strength, Tg, and Degradability
strength_txt = clean_text(row['Strength (GPa)'])
tg_txt = clean_text(row['Tg (Β°C)'])
degrad_txt = clean_text(row['Degradability'])
pdf.cell(0, 5, txt=f"Strength: {strength_txt} | Tg: {tg_txt}", ln=True)
pdf.cell(0, 5, txt=f"Degradability: {degrad_txt} | SA Score: {clean_text(row['SA Score'])}", ln=True)
pdf.ln(5)
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
pdf.ln(2)
pdf.output(pdf_path)
return csv_path, json_path, smiles_path, pdf_path
def on_refine_wrapper(seed_with_icon, num, mode, view, motifs, limit_aro, max_mw):
df, fig, dropdown, state, img, bar = on_refine_start(seed_with_icon, num, mode, view, motifs, limit_aro, max_mw)
# Same here: Change to string
init_msg = "Move sliders above to re-analyze refined batch."
init_anno = "### <span style='color:grey'>0% Pass Rate</span>"
return df, fig, dropdown, state, img, bar, init_msg, init_anno
# --- 6. GRADIO INTERFACE ---
with gr.Blocks(title="PolymerDesignAI") as demo:
gr.Markdown("<center><h1>π§ͺ PolymerDesignAI: Advanced Scientific Discovery</h1></center>")
with gr.Tab("Autonomous Design"):
gr.Markdown("### Integrated Generation + Screening")
# --- ROW 1: Optimization Mode & Strategy side-by-side ---
with gr.Row(variant="panel"):
app_mode = gr.Dropdown(
["Battery Mode", "Biomedical Mode", "Structural Mode"],
value="Biomedical Mode",
label="Select Optimization Mode"
)
toggle_view = gr.Radio(
["Single-objective ranking", "Multi-objective Pareto optimal (Best-in-Class candidates)"],
value="Single-objective ranking",
label="Selection Strategy"
)
# --- ROW 2: Three Text Blocks side-by-side ---
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("""**Application Mapping:**
* **Structural Mode**: Optimized Yield Strength & High Thermal Stability.
* **Battery Mode**: Optimized Glass Transition (Tg) & Conductivity.
* **Biomedical Mode**: Optimized Degradability & Biocompatibility.""")
with gr.Column(scale=1):
gr.Markdown("""**π Classification Logic:**
* **β
Excellent**: Passes all **4** benchmarks.
* **β οΈ Review**: Passes **2 or 3** benchmarks.
* **β Bad**: Passes **0 or 1** benchmark.""")
with gr.Column(scale=1):
gr.Markdown("""**π§ͺ Logic Benchmarks:**
* Benchmarks and thresholds auto-adjust per selected Mode.
* Incorporates Uncertainty (Β±) for scientific trust.""")
# --- ROW 3: Quantity + Constraints + Filters all in one line ---
with gr.Row(variant="compact", equal_height=True):
with gr.Column(scale=1):
num = gr.Slider(1, 50, 10, step=1, label="Quantity to Generate")
with gr.Column(scale=1):
must_have = gr.Dropdown(
["None", "Esters (Biodegradable)", "Carbonates (Biodegradable)",
"PLA-type (Lactide/Lactic)", "Anhydrides (Fast Degrading)",
"Amides (High Strength)", "Sulfones (Thermal Stability)",
"Urethanes", "Aryl-Monomer", "Aliphatic-Chain"],
value="None", label="π Lock Functional Groups", interactive=True
)
with gr.Column(scale=1):
exclude_aromatic = gr.Checkbox(label="Limit Aromatic (<20%)")
max_mol_wt = gr.Slider(100, 1200, 500, label="Max Monomer MW")
# --- ROW 4: Centered Run Button ---
# We use scale=1/2/1 to force the middle column (and button) to stay centered
with gr.Row():
gr.Markdown("<br>") # Top Spacer
with gr.Row():
with gr.Column(scale=1): pass
run = gr.Button("π Run Design Pipeline", variant="primary", size="lg", scale=2)
with gr.Column(scale=1): pass
with gr.Row():
gr.Markdown("<br>") # Bottom Spacer
pareto_plot = gr.Plot(label="Pareto Frontier")
gr.Markdown("### π AI-Generated Candidates")
with gr.Row():
with gr.Column(scale=2):
gr.Markdown("#### Filter by Status")
filter_btn_all = gr.Button("Show All", size="sm")
filter_btn_exc = gr.Button("Show Only Excellent β
", size="sm")
filter_btn_rev = gr.Button("Show Only Review β οΈ", size="sm")
with gr.Column(scale=2):
gr.Markdown("#### Quick Sort")
sort_dropdown = gr.Dropdown(
["Highest Strength", "Highest Tg", "Best Degradability", "Best SA (Easiest to make)", "Best Universal Score"],
label="Sort Candidates By",
interactive=True
)
# --- NEW: BENCHMARK SENSITIVITY ANALYSIS ---
with gr.Accordion("π Benchmark Sensitivity Analysis (Industrial What-if)", open=False):
gr.Markdown("Adjust thresholds to dynamically re-classify candidates and see market readiness.")
with gr.Row():
threshold_s = gr.Slider(0, 0.2, value=0.08, step=0.005, label="Min Strength (GPa)")
threshold_t = gr.Slider(0, 300, value=150, step=5, label="Min Tg (Β°C)")
threshold_d = gr.Slider(0, 1.0, value=0.10, step=0.05, label="Min Degradability")
with gr.Row():
pass_metric = gr.Markdown("### Market Readiness")
stats_box = gr.Markdown("### π Impact Summary")
# --------------------------------------------
results_df = gr.DataFrame(label="Candidate Table (Click headers to sort)")
full_results_state = gr.State()
with gr.Accordion("βοΈ Adjust Ranking Weights (Manual Override)", open=False):
gr.Markdown("Adjust these sliders to change how candidates are ranked in the table below.")
with gr.Row():
w_strength = gr.Slider(0, 100, 40, step=5, label="Strength Weight (%)")
w_tg = gr.Slider(0, 100, 30, step=5, label="Tg Weight (%)")
w_degrad = gr.Slider(0, 100, 20, step=5, label="Degradability Weight (%)")
w_sa = gr.Slider(0, 100, 10, step=5, label="SA Ease Weight (%)")
gr.Markdown(r"""**Weighted Rank Score Formula:** $$\text{Rank Score} = (S_{norm} \times W_S) + (T_{norm} \times W_T) + (D_{norm} \times W_D) + (SA_{penalty} \times W_{SA})$$""")
rank_btn = gr.Button("Calculate Custom Weighted Score", variant="secondary")
gr.Markdown("### π΅οΈ Candidate Deep-Dive & Explainability")
with gr.Row():
# Column 1: Selection & Radar
with gr.Column(scale=3):
selection_dropdown = gr.Dropdown(label="Select a SMILES to inspect", choices=[])
with gr.Row():
refine_btn = gr.Button("𧬠Refine Around This Candidate", variant="secondary")
radar_plot = gr.Plot(label="Performance Fingerprint")
with gr.Accordion("π How to read the Radar Plot", open=False):
gr.Markdown("""
* **Colored Area**: Selected polymer performance.
* **Dashed Grey Area**: Industry Benchmark targets.
* **Color Coding**: Green (Excellent), Yellow (Review), Red (Bad).
* **Scales**: Strength (0.15 GPa), Tg (300Β°C), Degradability (0-1).
""")
# Column 2: Structural Explainability (Point 1 of Phase 3)
with gr.Column(scale=3):
gr.Markdown("#### π Why This Molecule? (AI Reasoning)")
explain_mode = gr.Radio(
["Strength", "Tg", "Degradability"],
label="Select Property to Explain",
value="Strength"
)
explain_img = gr.Image(label="Substructure Contributions (Highlighted)", type="pil")
explain_bar = gr.Plot(label="Feature Impact Analysis (SHAP)")
# --- EXPORT HUB ---
gr.Markdown("### π₯ Export Results")
with gr.Row(variant="panel"):
download_csv = gr.File(label="CSV Spreadsheet")
download_json = gr.File(label="JSON Data")
download_smiles = gr.File(label="SMILES List (.txt)")
download_pdf = gr.File(label="Ranked Report (PDF)")
# Helper to trigger all exports at once when results change
def update_exports(df):
return generate_exports(df)
def on_gen_complete(num, mode, view, motifs, limit_aro, max_mw, progress=gr.Progress()):
progress(0, desc="π Initializing Generative AI...")
full_df, display_df, csv_path, fig = generate_and_screen(num, mode, view, motifs, limit_aro, max_mw)
def get_dropdown_icon(status_text):
if "Excellent" in status_text: return "π’"
if "Review" in status_text: return "π‘"
return "π΄"
dropdown_choices = [
f"{get_dropdown_icon(row['Classification'])} {row['p-SMILES']}"
for _, row in full_df.iterrows()
]
first_smiles = full_df["p-SMILES"].iloc[0] if not full_df.empty else None
first_choice = dropdown_choices[0] if dropdown_choices else None
img, bar = explain_polymer(first_smiles, "Strength")
return (display_df, fig,
gr.Dropdown(choices=dropdown_choices, value=first_choice), full_df, img, bar)
def on_refine_start(seed_with_icon, num, mode, view, motifs, limit_aro, max_mw):
if not seed_with_icon:
raise gr.Error("Please select a candidate from the dropdown first!")
seed = seed_with_icon.split(" ")[1] if " " in seed_with_icon else seed_with_icon
gr.Info(f"Evolving molecules similar to: {seed[:15]}... while respecting constraints")
full_df, display_df, csv_path, fig = generate_and_screen(num, mode, view, motifs=motifs, limit_aro=limit_aro, max_mw=max_mw, seed=seed)
# Use your new icon-logic for the refined dropdown results too
def get_dropdown_icon(status_text):
if "Excellent" in status_text: return "π’"
if "Review" in status_text: return "π‘"
return "π΄"
dropdown_choices = [
f"{get_dropdown_icon(row['Classification'])} {row['p-SMILES']}"
for _, row in full_df.iterrows()
]
first_smiles = full_df["p-SMILES"].iloc[0] if not full_df.empty else None
first_choice = dropdown_choices[0] if dropdown_choices else None
img, bar = explain_polymer(first_smiles, "Strength")
return (display_df, fig, gr.Dropdown(choices=dropdown_choices, value=first_choice), full_df, img, bar)
def filter_table(df, mode):
if df is None or df.empty:
return None
if mode == "Excellent":
filtered = df[df["Classification"].str.contains("Excellent")]
elif mode == "Review":
filtered = df[df["Classification"].str.contains("Review")]
else:
filtered = df
visible_cols = [
"p-SMILES",
"Strength (GPa)",
"Tg (Β°C)",
"Degradability",
"SA Score",
"Confidence",
"Classification"
]
if "Rank Score" in filtered.columns:
visible_cols.append("Rank Score")
return filtered[visible_cols]
def clean_and_update_radar(choice, df):
# Split by space and take the second part (the actual SMILES)
if not choice or df is None or df.empty:
return None
smiles = choice.split(" ")[1] if choice and " " in choice else choice
return update_radar_plot(smiles, df)
def clean_and_explain(choice, prop):
smiles = choice.split(" ")[1] if choice and " " in choice else choice
return explain_polymer(smiles, prop)
def on_gen_complete_wrapper(num, mode, view, motifs, limit_aro, max_mw, progress=gr.Progress()):
df, fig, dropdown, state, img, bar = on_gen_complete(num, mode, view, motifs, limit_aro, max_mw, progress)
# Change the list [("0% Pass Rate", "Awaiting")] to a simple string
init_msg = "### π Impact Summary\nMove sliders above to see analysis."
init_anno = "### <span style='color:grey'>0% Pass Rate</span>"
return df, fig, dropdown, state, img, bar, init_msg, init_anno
run.click(fn=on_gen_complete_wrapper, inputs=[num, app_mode, toggle_view, must_have, exclude_aromatic, max_mol_wt], outputs=[results_df, pareto_plot, selection_dropdown, full_results_state, explain_img, explain_bar, stats_box, pass_metric])
filter_btn_all.click(lambda x: filter_table(x, "All"), full_results_state, results_df)
filter_btn_exc.click(lambda x: filter_table(x, "Excellent"), full_results_state, results_df)
filter_btn_rev.click(lambda x: filter_table(x, "Review"), full_results_state, results_df)
# 2. When a user picks a SMILES from the dropdown, update the Radar Plot
selection_dropdown.change(
clean_and_update_radar,
[selection_dropdown, results_df],
radar_plot
)
selection_dropdown.change(
clean_and_explain,
[selection_dropdown, explain_mode],
[explain_img, explain_bar]
)
explain_mode.change(
clean_and_explain,
[selection_dropdown, explain_mode],
[explain_img, explain_bar]
)
sort_dropdown.change(sort_table, [full_results_state, sort_dropdown], results_df)
rank_btn.click(calculate_weighted_rank, inputs=[full_results_state, app_mode, w_strength, w_tg, w_degrad, w_sa], outputs=[full_results_state, results_df, download_csv])
refine_btn.click(
fn=on_refine_wrapper, # Points to the new 8-output wrapper
inputs=[selection_dropdown, num, app_mode, toggle_view, must_have, exclude_aromatic, max_mol_wt],
outputs=[results_df, pareto_plot, selection_dropdown, full_results_state, explain_img, explain_bar, stats_box, pass_metric]
)
# Sensitivity Triggers
slider_inputs = [full_results_state, threshold_s, threshold_t, threshold_d]
slider_outputs = [results_df, stats_box, pass_metric]
threshold_s.change(fn=run_sensitivity_analysis, inputs=slider_inputs, outputs=slider_outputs)
threshold_t.change(fn=run_sensitivity_analysis, inputs=slider_inputs, outputs=slider_outputs)
threshold_d.change(fn=run_sensitivity_analysis, inputs=slider_inputs, outputs=slider_outputs)
# Automatically update all download links whenever the results table changes
results_df.change(
fn=update_exports,
inputs=[results_df],
outputs=[download_csv, download_json, download_smiles, download_pdf]
)
# --- Inside the "Predict" Tab ---
with gr.Tab("Predict"):
gr.Markdown("### Forward Property Prediction")
with gr.Row():
with gr.Column():
pred_mode = gr.Radio(["Structural (Strength)", "Battery (Tg)", "Biomedical (Degradability)"],
value="Structural (Strength)", label="Select Property")
sm_in = gr.Textbox(label="Enter p-SMILES", placeholder="*CC*")
gr.Examples(
examples=[
["*CCCCCCNC(=O)NCCCCCCNC(=O)S*"],
["*Oc1ccc(C(C)(C)c2ccc(OC(=O)NCCCCCCNC(*)=O)cc2)cc1"],
["*[Se]c1ccc(C(=O)Nc2ccc(NC(=O)CCCCCCCC(=O)Nc3ccc(*)cc3)cc2)cc1"]
],
inputs=sm_in,
label="Quick Select Examples"
)
pred_btn = gr.Button("Predict Single", variant="primary")
res_label = gr.Label(label="Result")
with gr.Column():
gr.Markdown("#### Batch Processing")
file_in = gr.File(label="Upload CSV/XLSX (Must contain a 'smiles' column)")
batch_btn = gr.Button("Run Batch Prediction", variant="primary")
gr.Markdown("---")
gr.Markdown("#### Batch Results")
batch_df = gr.DataFrame(label="Preview (Top 10)")
# This is the new download component for the Predict tab
batch_file_out = gr.File(label="Download Predicted Results (CSV)")
# Logic for Single Prediction
pred_btn.click(predict_ui, [pred_mode, sm_in], res_label)
# Logic for Batch Prediction - mapping the return values to the dataframe and the file component
batch_btn.click(
fn=batch_predict,
inputs=[pred_mode, file_in],
outputs=[batch_df, batch_file_out]
)
# Launch the app with authentication
demo.launch(auth=(os.environ.get("app_user"), os.environ.get("app_pass")), ssr_mode=False) |