hohdavid commited on
Commit
c26bf25
·
verified ·
1 Parent(s): 3ababa5

Uploading initial files

Browse files
aligned_spaces_desalting.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:71e2e2b62eb0b31b864099186f43b1a37b69b1b5579ba0a61d8672abea611d4d
3
+ size 7853137
aligned_spaces_elution.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7632d4d415ab30995513f518799566e5461c2df8845cf37ea7408231cf708b18
3
+ size 7638722
aligned_spaces_lysis.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fb0e88bf7d8ad3317534b2d0177310dc082c9f02545c59a155a45c6518641f75
3
+ size 8387736
app.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ import pickle
6
+ import numpy as np
7
+ import re
8
+ from sklearn.cluster import KMeans
9
+ from transformers import T5Tokenizer, T5EncoderModel
10
+ import os
11
+ from dotenv import load_dotenv
12
+ import google.generativeai as genai
13
+
14
+ load_dotenv()
15
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
16
+
17
+ if not GEMINI_API_KEY:
18
+ st.error("API Key not found! Please check that your .env file exists and is formatted correctly.")
19
+ st.stop()
20
+
21
+ genai.configure(api_key=GEMINI_API_KEY)
22
+
23
+ sys_instruct = """
24
+ You are a Senior Research Biochemist. Your primary role is to design scientifically rigorous, highly cohesive protein purification pipelines based ONLY on provided laboratory data.
25
+
26
+ Follow these strict guidelines:
27
+ 1. Tone & Style: Be concise, professional, and direct. Omit conversational filler and pleasantries.
28
+ 2. Formatting: Use standard Markdown. Create clear headers for each step (e.g., '### Step 1: Lysis'). Bold all specific buffer concentrations, proteins, reagents, and pH values so they are easy to read at the bench.
29
+ 3. Biochemical Rationale: For every step, explicitly state *why* specific reagents are used based on standard biochemical principles.
30
+ 4. Chemical Guardrails: Never invent or hallucinate protocols or buffers. If you detect chemical incompatibilities in the user's request or retrieved data (e.g., high DTT concentrations applied to standard Ni-NTA columns, or inappropriate detergents for soluble proteins), explicitly flag them with a bold **WARNING**.
31
+ """
32
+
33
+ model = genai.GenerativeModel('gemini-3.5-flash',
34
+ system_instruction=sys_instruct)
35
+
36
+ st.set_page_config(
37
+ page_title="Bio-CLIP Recommender",
38
+ layout="wide",
39
+ initial_sidebar_state="expanded"
40
+ )
41
+
42
+ class BioCLIP(nn.Module):
43
+ def __init__(self, seq_dim=1024, text_dim=768, shared_dim=512):
44
+ super().__init__()
45
+ self.seq_projector = nn.Sequential(
46
+ nn.Linear(seq_dim, shared_dim),
47
+ nn.GELU(),
48
+ nn.Dropout(0.1),
49
+ nn.Linear(shared_dim, shared_dim)
50
+ )
51
+ self.text_projector = nn.Sequential(
52
+ nn.Linear(text_dim, shared_dim),
53
+ nn.GELU(),
54
+ nn.Dropout(0.1),
55
+ nn.Linear(shared_dim, shared_dim)
56
+ )
57
+
58
+ def forward(self, seq_emb, text_emb):
59
+ z_seq = F.normalize(self.seq_projector(seq_emb), p=2, dim=-1)
60
+ z_text = F.normalize(self.text_projector(text_emb), p=2, dim=-1)
61
+ return z_seq, z_text
62
+
63
+ class ProteinEmbedder:
64
+ def __init__(self, device="cpu"):
65
+ self.device = device
66
+ self.tokenizer = T5Tokenizer.from_pretrained("Rostlab/prot_t5_xl_uniref50", do_lower_case=False)
67
+ self.model = T5EncoderModel.from_pretrained("Rostlab/prot_t5_xl_uniref50").to(self.device)
68
+ self.model.eval()
69
+
70
+ def embed_raw_sequence(self, sequence: str):
71
+ seq = re.sub(r"[UZOB]", "X", sequence.upper())
72
+ seq_spaced = " ".join(list(seq))
73
+
74
+ with torch.no_grad():
75
+ ids = self.tokenizer.batch_encode_plus([seq_spaced], add_special_tokens=True, padding=True, return_tensors="pt")
76
+ input_ids = ids['input_ids'].to(self.device)
77
+ attention_mask = ids['attention_mask'].to(self.device)
78
+
79
+ embedding = self.model(input_ids=input_ids, attention_mask=attention_mask)
80
+ seq_len = (attention_mask[0] == 1).sum()
81
+ protein_emb = embedding.last_hidden_state[0, :seq_len-1].mean(dim=0)
82
+
83
+ return protein_emb.cpu().numpy()
84
+
85
+ class ProtocolRecommender:
86
+ def __init__(self, device="cpu"):
87
+ self.device = device
88
+ self.steps = ['lysis', 'elution', 'desalting']
89
+ self.models = {}
90
+ self.databases = {}
91
+
92
+ for step in self.steps:
93
+ model = BioCLIP().to(self.device)
94
+ model.load_state_dict(torch.load(f"bioclip_weights_{step}.pth", map_location=self.device, weights_only=True), strict=False)
95
+ model.eval()
96
+ self.models[step] = model
97
+
98
+ with open(f"aligned_spaces_{step}.pkl", "rb") as f:
99
+ db = pickle.load(f)
100
+
101
+ text_vectors_np = np.array(db["aligned_text"])
102
+ kmeans = KMeans(n_clusters=15, random_state=42, n_init=10)
103
+ cluster_labels = kmeans.fit_predict(text_vectors_np)
104
+
105
+ self.databases[step] = {
106
+ "text_vectors": torch.tensor(db["aligned_text"]).float().to(self.device),
107
+ "raw_texts": db["raw_texts"],
108
+ "cluster_labels": cluster_labels
109
+ }
110
+
111
+ def search(self, protein_sequence_1024d, top_k=3):
112
+ results = {}
113
+ seq_tensor = torch.tensor(protein_sequence_1024d).float().unsqueeze(0).to(self.device)
114
+
115
+ with torch.no_grad():
116
+ for step in self.steps:
117
+ model = self.models[step]
118
+ db = self.databases[step]
119
+
120
+ z_query = F.normalize(model.seq_projector(seq_tensor), p=2, dim=-1)
121
+
122
+ similarities = (z_query @ db["text_vectors"].T).squeeze().cpu().numpy()
123
+
124
+ sorted_indices = np.argsort(similarities)[::-1]
125
+
126
+ diverse_indices = []
127
+ seen_clusters = set()
128
+
129
+ for idx in sorted_indices:
130
+ cluster_id = db["cluster_labels"][idx]
131
+
132
+ if cluster_id not in seen_clusters:
133
+ seen_clusters.add(cluster_id)
134
+ diverse_indices.append(idx)
135
+
136
+ if len(diverse_indices) == top_k:
137
+ break
138
+
139
+ step_results = []
140
+ for idx in diverse_indices:
141
+ step_results.append({
142
+ "confidence": float(similarities[idx]),
143
+ "text": db["raw_texts"][idx]
144
+ })
145
+ results[step] = step_results
146
+ return results
147
+
148
+ @st.cache_resource(show_spinner=False)
149
+ def load_ai_engines():
150
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
151
+ embedder = ProteinEmbedder(device=device)
152
+ recommender = ProtocolRecommender(device=device)
153
+ return embedder, recommender
154
+
155
+ st.title("Bio-CLIP: Protein Purification AI")
156
+ st.markdown("Zero-shot protocol recommendation directly from 1D amino acid sequences.")
157
+
158
+ with st.spinner("Booting up ProtT5 and Bio-CLIP Models (this takes a moment)..."):
159
+ try:
160
+ embedder, recommender = load_ai_engines()
161
+ models_loaded = True
162
+ except Exception as e:
163
+ st.error(f"Failed to load model weights. Ensure `.pth` and `.pkl` files are in the same folder. \n\nError: {e}")
164
+ models_loaded = False
165
+
166
+ if models_loaded:
167
+ with st.sidebar:
168
+ st.header("About")
169
+ st.write("This tool uses **ProtT5** to embed amino acids and a Contrastive Learning (**Bio-CLIP**) model to map them to historical purification protocols.")
170
+ st.markdown("---")
171
+ st.write("**Top K Results**")
172
+ top_k = st.slider("Number of recommendations:", 1, 5, 3)
173
+
174
+ st.markdown("### Input Sequence")
175
+ default_gfp = "MSKGEELFTGVVPILVELDGDVNGHKFSVSGEGEGDATYGKLTLKFICTTGKLPVPWPTLVTTFSYGVQCFSRYPDHMKQHDFFKSAMPEGYVQERTIFFKDDGNYKTRAEVKFEGDTLVNRIELKGIDFKEDGNILGHKLEYNYNSHNVYIMADKQKNGIKVNFKIRHNIEDGSVQLADHYQQNTPIGDGPVLLPDNHYLSTQSALSKDPNEKRDHMVLLEFVTAAGITHGMDELYK"
176
+ sequence_input = st.text_area("Paste Amino Acid Sequence here:", value=default_gfp, height=150)
177
+
178
+ user_goal = st.text_input("Optional: What is your specific purification goal? (e.g., 'Prioritize highest yield', 'Are there temperature concerns?')")
179
+
180
+ if st.button("Generate Purification Pipeline", type="primary"):
181
+ sequence_input = sequence_input.strip()
182
+
183
+ if len(sequence_input) < 10:
184
+ st.warning("Please enter a valid amino acid sequence (at least 10 characters).")
185
+ else:
186
+ with st.spinner("1/2: Running ProtT5 Sequence Embedding..."):
187
+ seq_vector = embedder.embed_raw_sequence(sequence_input)
188
+
189
+ with st.spinner("2/2: Querying Bio-CLIP Latent Space..."):
190
+ results = recommender.search(seq_vector, top_k=top_k)
191
+
192
+ st.success("Search Complete!")
193
+ st.markdown("---")
194
+
195
+ col1, col2, col3 = st.columns(3)
196
+
197
+ columns = {'lysis': col1, 'elution': col2, 'desalting': col3}
198
+
199
+ for step in ['lysis', 'elution', 'desalting']:
200
+ with columns[step]:
201
+ st.subheader(f"{step.capitalize()}")
202
+ for i, res in enumerate(results[step]):
203
+
204
+ conf = max(0.0, min(1.0, (res['confidence'] + 0.1)))
205
+ st.markdown(f"**Option {i+1}**")
206
+ st.progress(conf, text=f"Confidence: {conf*100:.1f}%")
207
+ st.info(res['text'])
208
+ st.write("")
209
+
210
+ st.markdown("---")
211
+
212
+ st.markdown("### 3. AI Synthesis")
213
+ with st.spinner("3/3: Gemini AI analyzing retrieved protocols..."):
214
+ try:
215
+ retrieved_context = f"""
216
+ Lysis Options: {[res['text'] for res in results['lysis']]}
217
+ Elution Options: {[res['text'] for res in results['elution']]}
218
+ Desalting Options: {[res['text'] for res in results['desalting']]}
219
+ """
220
+ has_his_tag = "HHHHHH" in sequence_input
221
+
222
+ rag_prompt = f"""
223
+
224
+ User Sequence: {sequence_input}
225
+ Contains His-Tag: {has_his_tag}
226
+
227
+ Synthesize a cohesive, step-by-step protein purification protocol using ONLY the retrieved options below.
228
+ If the user provided a specific goal, tailor the synthesis to that goal.
229
+
230
+ User's specific goal/question: {user_goal if user_goal else "Provide a standard, recommended cohesive pipeline from these options."}
231
+
232
+ --- RETRIEVED PROTOCOL OPTIONS ---
233
+ {retrieved_context}
234
+ ----------------------------------
235
+ """
236
+
237
+ response = model.generate_content(rag_prompt)
238
+
239
+ st.write(response.text)
240
+
241
+ except Exception as e:
242
+ st.error(f"An error occurred while generating the AI summary: {e}")
bio_clip_recommender.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import pickle
5
+ import numpy as np
6
+ import re
7
+ from transformers import T5Tokenizer, T5EncoderModel
8
+
9
+ class BioCLIP(nn.Module):
10
+ def __init__(self, seq_dim=1024, text_dim=768, shared_dim=512):
11
+ super().__init__()
12
+ self.seq_projector = nn.Sequential(
13
+ nn.Linear(seq_dim, shared_dim),
14
+ nn.GELU(),
15
+ nn.Dropout(0.1),
16
+ nn.Linear(shared_dim, shared_dim)
17
+ )
18
+ self.text_projector = nn.Sequential(
19
+ nn.Linear(text_dim, shared_dim),
20
+ nn.GELU(),
21
+ nn.Dropout(0.1),
22
+ nn.Linear(shared_dim, shared_dim)
23
+ )
24
+ self.temperature = nn.Parameter(torch.ones([]) * np.log(1 / 0.07))
25
+
26
+ def forward(self, seq_emb, text_emb):
27
+ z_seq = F.normalize(self.seq_projector(seq_emb), p=2, dim=-1)
28
+ z_text = F.normalize(self.text_projector(text_emb), p=2, dim=-1)
29
+ return z_seq, z_text
30
+
31
+ class ProteinEmbedder:
32
+ def __init__(self, device="cpu"):
33
+ print("Loading ProtT5 Language Model (this takes a moment)...")
34
+ self.device = device
35
+ self.tokenizer = T5Tokenizer.from_pretrained("Rostlab/prot_t5_xl_uniref50", do_lower_case=False)
36
+ self.model = T5EncoderModel.from_pretrained("Rostlab/prot_t5_xl_uniref50").to(self.device)
37
+ self.model.eval()
38
+ print("ProtT5 Online!")
39
+
40
+ def embed_raw_sequence(self, sequence: str):
41
+ """Converts a raw string of amino acids into the 1024D vector."""
42
+ seq = re.sub(r"[UZOB]", "X", sequence.upper())
43
+
44
+ seq_spaced = " ".join(list(seq))
45
+
46
+ with torch.no_grad():
47
+ ids = self.tokenizer.batch_encode_plus([seq_spaced], add_special_tokens=True, padding=True, return_tensors="pt")
48
+ input_ids = ids['input_ids'].to(self.device)
49
+ attention_mask = ids['attention_mask'].to(self.device)
50
+
51
+ embedding = self.model(input_ids=input_ids, attention_mask=attention_mask)
52
+ embedding = embedding.last_hidden_state
53
+
54
+ seq_len = (attention_mask[0] == 1).sum()
55
+ protein_emb = embedding[0, :seq_len-1].mean(dim=0)
56
+
57
+ return protein_emb.cpu().numpy()
58
+
59
+ class ProtocolRecommender:
60
+ def __init__(self, device="cpu"):
61
+ self.device = device
62
+ self.steps = ['lysis', 'elution', 'desalting']
63
+ self.models = {}
64
+ self.databases = {}
65
+
66
+ print("Loading Bio-CLIP Expert Models...")
67
+ for step in self.steps:
68
+ model = BioCLIP().to(self.device)
69
+ weights_path = f"bioclip_weights_{step}.pth"
70
+ model.load_state_dict(torch.load(weights_path, map_location=self.device, weights_only=True))
71
+
72
+ model.eval()
73
+ self.models[step] = model
74
+
75
+ db_path = f"aligned_spaces_{step}.pkl"
76
+ with open(db_path, "rb") as f:
77
+ db = pickle.load(f)
78
+ self.databases[step] = {
79
+ "text_vectors": torch.tensor(db["aligned_text"]).float().to(self.device),
80
+ "raw_texts": db["raw_texts"]
81
+ }
82
+ print("Ready! All systems online.\n")
83
+
84
+ def search(self, protein_sequence_1024d, top_k=3):
85
+ results = {}
86
+
87
+ if not isinstance(protein_sequence_1024d, torch.Tensor):
88
+ seq_tensor = torch.tensor(protein_sequence_1024d).float().unsqueeze(0).to(self.device)
89
+ else:
90
+ seq_tensor = protein_sequence_1024d.to(self.device)
91
+ if seq_tensor.dim() == 1:
92
+ seq_tensor = seq_tensor.unsqueeze(0)
93
+
94
+ with torch.no_grad():
95
+ for step in self.steps:
96
+ model = self.models[step]
97
+ db = self.databases[step]
98
+
99
+ z_query = model.seq_projector(seq_tensor)
100
+ z_query = F.normalize(z_query, p=2, dim=-1)
101
+
102
+ similarities = (z_query @ db["text_vectors"].T).squeeze()
103
+
104
+ top_scores, top_indices = torch.topk(similarities, k=top_k)
105
+
106
+ step_results = []
107
+ for score, idx in zip(top_scores.cpu().numpy(), top_indices.cpu().numpy()):
108
+ step_results.append({
109
+ "confidence": score,
110
+ "text": db["raw_texts"][idx]
111
+ })
112
+ results[step] = step_results
113
+
114
+ return results
115
+
116
+ if __name__ == "__main__":
117
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
118
+
119
+ try:
120
+ embedder = ProteinEmbedder(device=device)
121
+ engine = ProtocolRecommender(device=device)
122
+
123
+ print("\n" + "="*60)
124
+ raw_amino_acids = "MSKGEELFTGVVPILVELDGDVNGHKFSVSGEGEGDATYGKLTLKFICTTGKLPVPWPTLVTTFSYGVQCFSRYPDHMKQHDFFKSAMPEGYVQERTIFFKDDGNYKTRAEVKFEGDTLVNRIELKGIDFKEDGNILGHKLEYNYNSHNVYIMADKQKNGIKVNFKIRHNIEDGSVQLADHYQQNTPIGDGPVLLPDNHYLSTQSALSKDPNEKRDHMVLLEFVTAAGITHGMDELYK"
125
+
126
+ print(f"User inputted sequence: {raw_amino_acids[:30]}... (Length: {len(raw_amino_acids)})")
127
+ print("="*60)
128
+
129
+ print("1. Passing sequence through ProtT5...")
130
+ new_protein_vector = embedder.embed_raw_sequence(raw_amino_acids)
131
+
132
+ print("2. Querying Bio-CLIP databases...\n")
133
+ recommendations = engine.search(new_protein_vector, top_k=3)
134
+
135
+ print("="*60)
136
+ print("BIO-CLIP RECOMMENDED PURIFICATION PIPELINE")
137
+ print("="*60)
138
+
139
+ for step in ['lysis', 'elution', 'desalting']:
140
+ print(f"\n--- {step.upper()} EXPERT ---")
141
+ for i, rec in enumerate(recommendations[step]):
142
+ confidence_pct = max(0, min(100, (rec['confidence'] + 0.1) * 100))
143
+
144
+ print(f"Option {i+1} [Confidence: {confidence_pct:.1f}%]")
145
+ print(f"Protocol: {rec['text']}\n")
146
+
147
+ except FileNotFoundError as e:
148
+ print(f"\nERROR: Could not find model files. Make sure you run this in the same folder as your .pth and .pkl files!")
149
+ print(f"Details: {e}")
bioclip_weights_desalting.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13a4a32a180a3eb00b9612ae54e182bab32dd32597310d4023781520954c3b88
3
+ size 5779494
bioclip_weights_elution.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:723e1c03c258370db65503a908d51a10d6e3c571acfe6e8eb041c22bee846df5
3
+ size 5779468
bioclip_weights_lysis.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0591cfc98bdaebb86a608dc2efbc29a339a210fa7958f480d850a45eed862e6a
3
+ size 5779378