KangjieXu commited on
Commit
8b8182d
·
verified ·
1 Parent(s): cf5d567

Upload 8 files

Browse files
Files changed (8) hide show
  1. Dockerfile +15 -0
  2. README.md +11 -10
  3. app.py +72 -0
  4. model.py +277 -0
  5. requirements.txt +15 -0
  6. static/script.js +34 -0
  7. static/style.css +10 -0
  8. templates/index.html +32 -0
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # 安装底层系统依赖(RDKit 和 编译工具)
6
+ RUN apt-get update && apt-get install -y \
7
+ git \
8
+ git-lfs \
9
+ build-essential \
10
+ libgl1 \
11
+ libglib2.0-0 \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # 锁定 NumPy 1.x 和 Torch 2.4.1 (CPU版)
15
+ RUN pip install --no-cache-dir "numpy<2"
README.md CHANGED
@@ -1,10 +1,11 @@
1
- ---
2
- title: CASKP
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
+ ---
2
+ title: CASKP Predictor
3
+ emoji: 🧬
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # CASKP: Physics-Informed kcat Predictor for β-CAs
11
+ This model integrates **ESM-2** embeddings with **Rosetta Docking Scores** to predict the kcat of β-Carbonic Anhydrases.
app.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ import torch
3
+ from transformers import AutoTokenizer
4
+ from huggingface_hub import hf_hub_download
5
+ import os
6
+ from model import FullKcatPredictor # 确保 model.py 在同级目录
7
+
8
+ app = Flask(__name__)
9
+
10
+ # --- 配置 ---
11
+ DEVICE = torch.device("cpu")
12
+ ESM_MODEL_NAME = "facebook/esm2_t33_650M_UR50D"
13
+ REPO_ID = "KangjieXu/CASKP-model" # 修改为你的模型仓库
14
+
15
+ MODEL = None
16
+ TOKENIZER = None
17
+
18
+ def load_model():
19
+ global MODEL, TOKENIZER
20
+ TOKENIZER = AutoTokenizer.from_pretrained(ESM_MODEL_NAME)
21
+
22
+ # 从 Hub 下载权重
23
+ weights_path = hf_hub_download(repo_id=REPO_ID, filename="caskp_final_model.pt")
24
+
25
+ MODEL = FullKcatPredictor(
26
+ esm_model_name=ESM_MODEL_NAME,
27
+ struct_dim=1,
28
+ d_model=256,
29
+ d_multiscale=128,
30
+ num_heads=8,
31
+ use_amsff=True
32
+ )
33
+ MODEL.load_state_dict(torch.load(weights_path, map_location=DEVICE))
34
+ MODEL.eval()
35
+
36
+ @app.route('/')
37
+ def index():
38
+ return render_template('index.html')
39
+
40
+ @app.route('/predict', methods=['POST'])
41
+ def predict():
42
+ try:
43
+ data = request.json
44
+ sequence = data.get('sequence', '').strip().upper()
45
+ struct_val = float(data.get('score', -7.5)) # 默认值
46
+
47
+ if not sequence:
48
+ return jsonify({'error': 'Sequence is empty'})
49
+
50
+ # 推理逻辑
51
+ inputs = TOKENIZER(sequence, return_tensors='pt', padding="max_length", max_length=512, truncation=True)
52
+ struct_features = torch.tensor([[struct_val]], dtype=torch.float)
53
+
54
+ with torch.no_grad():
55
+ log_kcat = MODEL(
56
+ input_ids=inputs['input_ids'],
57
+ attention_mask=inputs['attention_mask'],
58
+ struct_features=struct_features
59
+ ).item()
60
+
61
+ return jsonify({
62
+ 'kcat': round(10**log_kcat, 4),
63
+ 'log_kcat': round(log_kcat, 4),
64
+ 'status': 'success'
65
+ })
66
+ except Exception as e:
67
+ return jsonify({'error': str(e)})
68
+
69
+ if __name__ == '__main__':
70
+ load_model()
71
+ # HF Spaces 必须监听 7860 端口
72
+ app.run(host='0.0.0.0', port=7860)
model.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from transformers import EsmModel
4
+ from torch_geometric.nn import GATv2Conv
5
+ from torch_geometric.data import Data, Batch
6
+ from rdkit import Chem
7
+ from rdkit.Chem import AllChem
8
+
9
+ # --- Helper Functions for Graph Creation ---
10
+ def get_atom_features(atom):
11
+ possible_atoms = ['C', 'O', 'N', 'S', 'F', 'Cl', 'Br', 'I', 'P', 'Co', 'Fe', 'Cu', 'Zn', 'Mg', 'Mn', 'Cr', 'Ni']
12
+ features = [0] * (len(possible_atoms) + 1)
13
+ try:
14
+ idx = possible_atoms.index(atom.GetSymbol())
15
+ features[idx] = 1
16
+ except ValueError:
17
+ features[-1] = 1
18
+ return features
19
+
20
+ def get_bond_features(bond):
21
+ bond_type = bond.GetBondType()
22
+ return [
23
+ bond_type == Chem.rdchem.BondType.SINGLE,
24
+ bond_type == Chem.rdchem.BondType.DOUBLE,
25
+ bond_type == Chem.rdchem.BondType.TRIPLE,
26
+ bond_type == Chem.rdchem.BondType.AROMATIC
27
+ ]
28
+
29
+ def smiles_to_pyg_graph(smiles_string):
30
+ """
31
+ Converts a SMILES string into a PyTorch Geometric Data object.
32
+ Returns None if the SMILES string is invalid.
33
+ """
34
+ try:
35
+ mol = Chem.MolFromSmiles(smiles_string)
36
+ if mol is None: return None
37
+ mol = Chem.AddHs(mol)
38
+ AllChem.EmbedMolecule(mol, AllChem.ETKDG())
39
+
40
+ atom_features_list = [get_atom_features(atom) for atom in mol.GetAtoms()]
41
+ x = torch.tensor(atom_features_list, dtype=torch.float)
42
+
43
+ if mol.GetNumBonds() > 0:
44
+ edge_indices, edge_attrs = [], []
45
+ for bond in mol.GetBonds():
46
+ i = bond.GetBeginAtomIdx()
47
+ j = bond.GetEndAtomIdx()
48
+ edge_indices.append((i, j))
49
+ edge_indices.append((j, i))
50
+ bond_features = get_bond_features(bond)
51
+ edge_attrs.append(bond_features)
52
+ edge_attrs.append(bond_features)
53
+
54
+ edge_index = torch.tensor(edge_indices, dtype=torch.long).t().contiguous()
55
+ edge_attr = torch.tensor(edge_attrs, dtype=torch.float)
56
+ else:
57
+ edge_index = torch.empty((2, 0), dtype=torch.long)
58
+ edge_attr = torch.empty((0, 4), dtype=torch.float)
59
+
60
+ return Data(x=x, edge_index=edge_index, edge_attr=edge_attr)
61
+
62
+ except Exception:
63
+ return None
64
+
65
+ # =====================================================================================
66
+ # == TransKP Model and Components (Locked) ==
67
+ # =====================================================================================
68
+
69
+ class SubstrateGNN(nn.Module):
70
+ """
71
+ Graph Attention Network (GATv2) for processing substrate SMILES strings.
72
+ """
73
+ def __init__(self, input_dim, hidden_dim, output_dim, heads=4, dropout=0.1):
74
+ super(SubstrateGNN, self).__init__()
75
+ self.conv1 = GATv2Conv(input_dim, hidden_dim, heads=heads, dropout=dropout, concat=True)
76
+ self.conv2 = GATv2Conv(hidden_dim * heads, hidden_dim, heads=heads, dropout=dropout, concat=True)
77
+ self.conv3 = GATv2Conv(hidden_dim * heads, output_dim, heads=1, dropout=dropout, concat=False)
78
+ self.elu = nn.ELU()
79
+ self.dropout = nn.Dropout(p=dropout)
80
+
81
+ def forward(self, data):
82
+ x, edge_index = data.x, data.edge_index
83
+ x = self.dropout(self.elu(self.conv1(x, edge_index)))
84
+ x = self.dropout(self.elu(self.conv2(x, edge_index)))
85
+ x = self.conv3(x, edge_index)
86
+
87
+ if hasattr(data, 'batch') and data.batch is not None:
88
+ from torch_geometric.nn import global_mean_pool
89
+ graph_embedding = global_mean_pool(x, data.batch)
90
+ else:
91
+ graph_embedding = x.mean(dim=0, keepdim=True)
92
+
93
+ return graph_embedding
94
+
95
+ class FusionBlock(nn.Module):
96
+ """
97
+ A single block for cross-modal fusion, combining self-attention and cross-attention.
98
+ """
99
+ def __init__(self, d_model, num_heads, dim_feedforward, dropout=0.1):
100
+ super(FusionBlock, self).__init__()
101
+ self.self_attn_protein = nn.MultiheadAttention(d_model, num_heads, dropout=dropout, batch_first=True)
102
+ self.cross_attn_prot_to_sub = nn.MultiheadAttention(d_model, num_heads, dropout=dropout, batch_first=True)
103
+ self.ffn_protein = nn.Sequential(
104
+ nn.Linear(d_model, dim_feedforward), nn.ReLU(), nn.Dropout(dropout),
105
+ nn.Linear(dim_feedforward, d_model), nn.Dropout(dropout)
106
+ )
107
+ self.norm1 = nn.LayerNorm(d_model)
108
+ self.norm2 = nn.LayerNorm(d_model)
109
+ self.norm3 = nn.LayerNorm(d_model)
110
+
111
+ def forward(self, protein_emb, substrate_emb, protein_mask=None):
112
+ protein_emb = self.norm1(protein_emb + self._sa_block(protein_emb, protein_mask))
113
+ protein_emb = self.norm2(protein_emb + self._ca_block(protein_emb, substrate_emb))
114
+ protein_emb = self.norm3(protein_emb + self.ffn_protein(protein_emb))
115
+ return protein_emb
116
+
117
+ def _sa_block(self, x, key_padding_mask):
118
+ x, _ = self.self_attn_protein(x, x, x, key_padding_mask=key_padding_mask)
119
+ return x
120
+
121
+ def _ca_block(self, query, key_value):
122
+ x, _ = self.cross_attn_prot_to_sub(query, key_value, key_value)
123
+ return x
124
+
125
+ class DeepFusionKcatPredictor(nn.Module):
126
+ """
127
+ The TransKP model that integrates ESM-2 for protein sequences and a GNN for substrates.
128
+ """
129
+ def __init__(self, esm_model_name, gnn_input_dim, gnn_hidden_dim, gnn_heads, d_model,
130
+ num_fusion_blocks, num_attn_heads, dim_feedforward, dropout=0.1):
131
+ super(DeepFusionKcatPredictor, self).__init__()
132
+
133
+ self.esm_model = EsmModel.from_pretrained(esm_model_name)
134
+ self.protein_projection = nn.Linear(self.esm_model.config.hidden_size, d_model)
135
+ self.gnn = SubstrateGNN(input_dim=gnn_input_dim, hidden_dim=gnn_hidden_dim, output_dim=d_model, heads=gnn_heads)
136
+ self.fusion_blocks = nn.ModuleList([
137
+ FusionBlock(d_model, num_attn_heads, dim_feedforward, dropout) for _ in range(num_fusion_blocks)
138
+ ])
139
+
140
+ self.output_regressor = nn.Sequential(
141
+ nn.Linear(d_model, d_model // 2),
142
+ nn.ReLU(),
143
+ nn.Dropout(dropout),
144
+ nn.Linear(d_model // 2, 1)
145
+ )
146
+
147
+ def forward(self, input_ids, attention_mask, smiles_list):
148
+ batch_size = input_ids.shape[0]
149
+ device = input_ids.device
150
+ final_predictions = torch.zeros(batch_size, device=device, dtype=torch.float32)
151
+
152
+ graphs = [smiles_to_pyg_graph(s) for s in smiles_list]
153
+ valid_indices = [i for i, g in enumerate(graphs) if g is not None]
154
+
155
+ if valid_indices:
156
+ valid_graphs = [graphs[i] for i in valid_indices]
157
+ graph_batch = Batch.from_data_list(valid_graphs).to(device)
158
+
159
+ substrate_embedding = self.gnn(graph_batch).unsqueeze(1)
160
+
161
+ valid_input_ids = input_ids[valid_indices]
162
+ valid_attention_mask = attention_mask[valid_indices]
163
+ esm_outputs = self.esm_model(input_ids=valid_input_ids, attention_mask=valid_attention_mask)
164
+ protein_embedding = self.protein_projection(esm_outputs.last_hidden_state)
165
+
166
+ fused_output = protein_embedding
167
+ key_padding_mask = (valid_attention_mask == 0)
168
+ for block in self.fusion_blocks:
169
+ fused_output = block(fused_output, substrate_embedding, protein_mask=key_padding_mask)
170
+
171
+ masked_fused_output = fused_output * valid_attention_mask.unsqueeze(-1)
172
+ summed_output = masked_fused_output.sum(dim=1)
173
+ non_pad_count = valid_attention_mask.sum(dim=1, keepdim=True)
174
+ pooled_output = summed_output / non_pad_count.clamp(min=1e-9)
175
+
176
+ predicted_kcat = self.output_regressor(pooled_output).squeeze(-1)
177
+ final_predictions[valid_indices] = predicted_kcat.to(torch.float32)
178
+
179
+ return final_predictions
180
+
181
+ # =====================================================================================
182
+ # == CASKP Model and Components (New Code) ==
183
+ # =====================================================================================
184
+
185
+ class AMSFF(nn.Module):
186
+ """
187
+ Adaptive Multi-Scale Feature Fusion (AMSFF) block.
188
+ Extracts multi-scale features from sequence embeddings using 1D convolutions.
189
+ """
190
+ def __init__(self, d_model, d_multiscale, dropout=0.1):
191
+ super(AMSFF, self).__init__()
192
+ self.d_model = d_model
193
+
194
+ self.conv_k3 = nn.Conv1d(d_model, d_multiscale, kernel_size=3, padding=1)
195
+ self.conv_k9 = nn.Conv1d(d_model, d_multiscale, kernel_size=9, padding=4)
196
+ self.conv_k21 = nn.Conv1d(d_model, d_multiscale, kernel_size=21, padding=10)
197
+
198
+ self.relu = nn.ReLU()
199
+ self.dropout = nn.Dropout(dropout)
200
+
201
+ self.projection = nn.Linear(d_multiscale * 3, d_model)
202
+
203
+ def forward(self, seq_embedding):
204
+ x = seq_embedding.transpose(1, 2)
205
+
206
+ h_local = self.relu(self.conv_k3(x))
207
+ h_medium = self.relu(self.conv_k9(x))
208
+ h_global = self.relu(self.conv_k21(x))
209
+
210
+ h_multi_scale = torch.cat([h_local, h_medium, h_global], dim=1)
211
+ h_multi_scale = h_multi_scale.transpose(1, 2)
212
+
213
+ projected_features = self.dropout(self.projection(h_multi_scale))
214
+
215
+ return projected_features
216
+
217
+ class HyperAttention(nn.Module):
218
+ """
219
+ HyperAttention Fusion block.
220
+ Fuses sequence and structure embeddings using spatial cross-attention.
221
+ """
222
+ def __init__(self, d_model, struct_dim, num_heads=8, dropout=0.1):
223
+ super(HyperAttention, self).__init__()
224
+ self.d_model = d_model
225
+
226
+ self.spatial_attention = nn.MultiheadAttention(d_model, num_heads, dropout=dropout, batch_first=True)
227
+ self.struct_projection = nn.Linear(struct_dim, d_model)
228
+ self.norm1 = nn.LayerNorm(d_model)
229
+
230
+ def forward(self, seq_embedding, struct_features):
231
+ struct_kv = self.struct_projection(struct_features).unsqueeze(1)
232
+ spatial_out, _ = self.spatial_attention(seq_embedding, struct_kv, struct_kv)
233
+ fused_embedding = self.norm1(seq_embedding + spatial_out)
234
+
235
+ return fused_embedding
236
+
237
+ class FullKcatPredictor(nn.Module):
238
+ """
239
+ The CASKP model, integrating ESM-2, AMSFF, HyperAttention, and a regressor.
240
+ """
241
+ def __init__(self, esm_model_name, struct_dim, d_model=256, d_multiscale=128, num_heads=8, dropout=0.1, use_amsff=True):
242
+ super(FullKcatPredictor, self).__init__()
243
+ self.use_amsff = use_amsff
244
+
245
+ self.esm_model = EsmModel.from_pretrained(esm_model_name)
246
+ self.protein_projection = nn.Linear(self.esm_model.config.hidden_size, d_model)
247
+
248
+ if self.use_amsff:
249
+ self.amsff = AMSFF(d_model, d_multiscale, dropout)
250
+
251
+ self.hyper_attention = HyperAttention(d_model, struct_dim, num_heads, dropout)
252
+
253
+ self.output_regressor = nn.Sequential(
254
+ nn.Linear(d_model, d_model // 2),
255
+ nn.ReLU(),
256
+ nn.Dropout(dropout),
257
+ nn.Linear(d_model // 2, 1)
258
+ )
259
+
260
+ def forward(self, input_ids, attention_mask, struct_features):
261
+ esm_outputs = self.esm_model(input_ids=input_ids, attention_mask=attention_mask)
262
+ protein_embedding = self.protein_projection(esm_outputs.last_hidden_state)
263
+
264
+ if self.use_amsff:
265
+ seq_feat_multiscale = self.amsff(protein_embedding)
266
+ fused_output = self.hyper_attention(seq_feat_multiscale, struct_features)
267
+ else:
268
+ fused_output = self.hyper_attention(protein_embedding, struct_features)
269
+
270
+ masked_fused_output = fused_output * attention_mask.unsqueeze(-1)
271
+ summed_output = masked_fused_output.sum(dim=1)
272
+ non_pad_count = attention_mask.sum(dim=1, keepdim=True)
273
+ pooled_output = summed_output / non_pad_count.clamp(min=1e-9)
274
+
275
+ predicted_kcat = self.output_regressor(pooled_output)
276
+
277
+ return predicted_kcat
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ huggingface_hub>=0.28.0
2
+ transformers>=4.48.0
3
+ numpy<2
4
+ xgboost
5
+ scikit-learn
6
+ flask
7
+ rdkit
8
+
9
+ # 图神经网络组件 (适配 Torch 2.4)
10
+ --find-links https://data.pyg.org/whl/torch-2.4.0+cpu.html
11
+ torch_geometric
12
+ torch-scatter
13
+ torch-sparse
14
+ torch-cluster
15
+ torch-spline-conv
static/script.js ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ async function runPrediction() {
2
+ const seq = document.getElementById('sequence').value;
3
+ const score = document.getElementById('score').value;
4
+ const btn = document.getElementById('btn');
5
+ const resBox = document.getElementById('result');
6
+
7
+ if(!seq) { alert("Please enter a sequence!"); return; }
8
+
9
+ btn.innerText = "Processing...";
10
+ btn.disabled = true;
11
+
12
+ try {
13
+ const response = await fetch('/predict', {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({ sequence: seq, score: score })
17
+ });
18
+
19
+ const data = await response.json();
20
+
21
+ if(data.status === 'success') {
22
+ document.getElementById('kcat_val').innerText = data.kcat;
23
+ document.getElementById('log_kcat_val').innerText = data.log_kcat;
24
+ resBox.style.display = "block";
25
+ } else {
26
+ alert("Error: " + data.error);
27
+ }
28
+ } catch (e) {
29
+ alert("Request failed!");
30
+ } finally {
31
+ btn.innerText = "Predict kcat";
32
+ btn.disabled = false;
33
+ }
34
+ }
static/style.css ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f4f7f6; display: flex; justify-content: center; padding: 50px; }
2
+ .container { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); width: 100%; max-width: 600px; }
3
+ h1 { color: #2c3e50; text-align: center; }
4
+ .input-group { margin-bottom: 20px; }
5
+ label { display: block; margin-bottom: 8px; font-weight: bold; color: #34495e; }
6
+ textarea { width: 100%; height: 120px; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; }
7
+ input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; }
8
+ button { width: 100%; padding: 12px; background: #27ae60; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; }
9
+ button:hover { background: #219150; }
10
+ .result-box { margin-top: 30px; padding: 20px; background: #e8f6ef; border-radius: 8px; border-left: 5px solid #27ae60; }
templates/index.html ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>CASKP Predictor</title>
5
+ <link rel="stylesheet" href="/static/style.css">
6
+ </head>
7
+ <body>
8
+ <div class="container">
9
+ <h1>🧬 CASKP Predictor</h1>
10
+ <p>Physics-Informed kcat Prediction for β-CAs</p>
11
+
12
+ <div class="input-group">
13
+ <label>Protein Sequence:</label>
14
+ <textarea id="sequence" placeholder="Enter amino acid sequence (e.g., MSK...)"></textarea>
15
+ </div>
16
+
17
+ <div class="input-group">
18
+ <label>Rosetta Docking Score (Physics Prior):</label>
19
+ <input type="number" id="score" value="-7.5" step="0.1">
20
+ </div>
21
+
22
+ <button onclick="runPrediction()" id="btn">Predict kcat</button>
23
+
24
+ <div id="result" class="result-box" style="display:none;">
25
+ <h3>Prediction Results:</h3>
26
+ <p>kcat (s⁻¹): <strong id="kcat_val">-</strong></p>
27
+ <p>log10(kcat): <strong id="log_kcat_val">-</strong></p>
28
+ </div>
29
+ </div>
30
+ <script src="/static/script.js"></script>
31
+ </body>
32
+ </html>