Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -9,14 +9,14 @@ from rdkit import Chem
|
|
| 9 |
from rdkit.Chem.Draw import rdMolDraw2D
|
| 10 |
from huggingface_hub import hf_hub_download
|
| 11 |
|
| 12 |
-
# 导入网络结构
|
| 13 |
from IR_encoder import IRModel
|
| 14 |
from SMILES_encoder import SmilesModel
|
|
|
|
|
|
|
| 15 |
from ir_process import (
|
| 16 |
preprocess_jdx_spectra_higer_500,
|
| 17 |
-
|
| 18 |
-
preprocess_csv_spectra_higer_500,
|
| 19 |
-
preprocess_csv_spectra_lower_than_500
|
| 20 |
)
|
| 21 |
|
| 22 |
# ==================== 全局配置 ====================
|
|
@@ -117,10 +117,10 @@ def load_model_instance(model_instance, weight_filename, device):
|
|
| 117 |
return model_instance
|
| 118 |
|
| 119 |
|
| 120 |
-
# ==================== 1.
|
| 121 |
print("Initializing Models and Feature Libraries...")
|
| 122 |
|
| 123 |
-
# 通用
|
| 124 |
ir_model_general = load_model_instance(IRModel(), "overall_best_top1_ir.pth", DEVICE)
|
| 125 |
smiles_model_general = load_model_instance(
|
| 126 |
SmilesModel(roberta_model_path=None, roberta_tokenizer_path=TOKENIZER_PATH, smiles_maxlen=300, feature_dim=768),
|
|
@@ -128,7 +128,7 @@ smiles_model_general = load_model_instance(
|
|
| 128 |
DEVICE
|
| 129 |
)
|
| 130 |
|
| 131 |
-
# 专属 PS
|
| 132 |
ir_model_ps = load_model_instance(IRModel(), "overall_best_nps_recall10_ir.pth", DEVICE)
|
| 133 |
smiles_model_ps = load_model_instance(
|
| 134 |
SmilesModel(roberta_model_path=None, roberta_tokenizer_path=TOKENIZER_PATH, smiles_maxlen=300, feature_dim=768),
|
|
@@ -136,7 +136,7 @@ smiles_model_ps = load_model_instance(
|
|
| 136 |
DEVICE
|
| 137 |
)
|
| 138 |
|
| 139 |
-
# 载入分子
|
| 140 |
library_nist_features = load_gz_features("library_NIST_smiles_features_fp16.gz", DEVICE)
|
| 141 |
general_smiles_nist = load_smiles_list("NIST_smiles_can.txt")
|
| 142 |
|
|
@@ -153,24 +153,17 @@ ALL_LIBS = {
|
|
| 153 |
}
|
| 154 |
|
| 155 |
|
| 156 |
-
# ==================== 2. 置信度
|
| 157 |
def calculate_calibrated_confidence(cosine_sim: float, smiles_model_instance, model_type: str = "general") -> float:
|
| 158 |
-
"""
|
| 159 |
-
使用从离线测试集拟合的 Platt Scaling 参数计算校准概率
|
| 160 |
-
"""
|
| 161 |
with torch.no_grad():
|
| 162 |
t = torch.exp(smiles_model_instance.t_prime).cpu().item()
|
| 163 |
b = smiles_model_instance.bias.cpu().item()
|
| 164 |
-
|
| 165 |
-
# 1. 计算双塔模型原始 Logit
|
| 166 |
raw_logit = cosine_sim * t + b
|
| 167 |
-
|
| 168 |
-
# 2. 读取对应模型的 Platt A 与 B 参数
|
| 169 |
params = PLATT_CALIBRATION_PARAMS.get(model_type, PLATT_CALIBRATION_PARAMS["general"])
|
| 170 |
A = params["A"]
|
| 171 |
B = params["B"]
|
| 172 |
-
|
| 173 |
-
# 3. 逻辑回归推断: P = 1 / (1 + exp(-(A * Logit + B)))
|
| 174 |
calibrated_logit = A * raw_logit + B
|
| 175 |
confidence = 1.0 / (1.0 + np.exp(-calibrated_logit))
|
| 176 |
return float(np.clip(confidence, 0.0, 1.0))
|
|
@@ -187,7 +180,7 @@ def process_custom_smiles(smiles_str, smiles_model_instance):
|
|
| 187 |
original_map[can] = s
|
| 188 |
if not valid_smiles:
|
| 189 |
return None, [], []
|
| 190 |
-
|
| 191 |
tokenizer = smiles_model_instance.smiles_tokenizer
|
| 192 |
encoded = [tokenizer(s, max_length=300, padding='max_length', truncation=True, return_tensors='pt') for s in valid_smiles]
|
| 193 |
ids = torch.cat([item['input_ids'] for item in encoded], dim=0).to(DEVICE)
|
|
@@ -197,7 +190,7 @@ def process_custom_smiles(smiles_str, smiles_model_instance):
|
|
| 197 |
return feats, valid_smiles, [original_map[s] for s in valid_smiles]
|
| 198 |
|
| 199 |
|
| 200 |
-
# ==================== 3. 检索主
|
| 201 |
def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_str, access_key, progress=gr.Progress()):
|
| 202 |
if library_name == "PS Derivative Library":
|
| 203 |
if access_key != ADMIN_KEY:
|
|
@@ -206,7 +199,7 @@ def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_st
|
|
| 206 |
if ir_file is None:
|
| 207 |
raise gr.Error("Please upload an IR spectrum file.")
|
| 208 |
|
| 209 |
-
# 动态切换
|
| 210 |
is_ps_lib = library_name in ["PS Existing Library", "PS Derivative Library"]
|
| 211 |
current_ir_model = ir_model_ps if is_ps_lib else ir_model_general
|
| 212 |
current_smiles_model = smiles_model_ps if is_ps_lib else smiles_model_general
|
|
@@ -218,34 +211,27 @@ def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_st
|
|
| 218 |
if file_path.lower().endswith('.csv'):
|
| 219 |
try:
|
| 220 |
df = pd.read_csv(file_path, header=None)
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
ir_data = preprocess_csv_spectra_higer_500(w, i) if w[0] > 500 else preprocess_csv_spectra_lower_than_500(w, i)
|
| 224 |
-
else:
|
| 225 |
-
ir_data = preprocess_csv_spectra_higer_500(w, i)
|
| 226 |
except Exception as e:
|
| 227 |
raise gr.Error(f"Error processing CSV: {str(e)}")
|
| 228 |
|
| 229 |
elif file_path.lower().endswith('.jdx'):
|
| 230 |
try:
|
| 231 |
data = jcamp.jcamp_readfile(file_path)
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
ir_data = preprocess_jdx_spectra_higer_500(w, i) if w[0] > 500 else preprocess_jdx_spectra_lower_500(w, i)
|
| 235 |
-
else:
|
| 236 |
-
ir_data = preprocess_jdx_spectra_higer_500(w, i)
|
| 237 |
except Exception as e:
|
| 238 |
raise gr.Error(f"Error processing JDX: {str(e)}")
|
| 239 |
|
| 240 |
if ir_data is None:
|
| 241 |
-
raise gr.Error("Unsupported file format or invalid
|
| 242 |
|
| 243 |
-
#
|
| 244 |
ir_tensor = torch.tensor(ir_data, dtype=torch.float32).unsqueeze(0).to(DEVICE)
|
| 245 |
with torch.no_grad():
|
| 246 |
ir_feat = current_ir_model(ir_tensor).to(torch.float32)
|
| 247 |
|
| 248 |
-
# 匹配库读取
|
| 249 |
if library_name == "Custom Library":
|
| 250 |
lib_features, lib_smiles, display_smiles = process_custom_smiles(custom_smiles_str, current_smiles_model)
|
| 251 |
if lib_features is None:
|
|
@@ -254,17 +240,16 @@ def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_st
|
|
| 254 |
lib_features, lib_smiles = ALL_LIBS[library_name]
|
| 255 |
display_smiles = lib_smiles
|
| 256 |
|
| 257 |
-
# 相似度矩阵与 Top10
|
| 258 |
sims = ir_feat @ lib_features.to(DEVICE).to(torch.float32).t()
|
| 259 |
scores, indices = sims.topk(min(10, lib_features.shape[0]), dim=1)
|
| 260 |
res_smiles = [display_smiles[idx.item()] for idx in indices[0]]
|
| 261 |
res_scores = [sco.item() for sco in scores[0]]
|
| 262 |
|
| 263 |
-
# 置信度
|
| 264 |
top1_score = res_scores[0]
|
| 265 |
top1_calibrated_conf = calculate_calibrated_confidence(top1_score, current_smiles_model, model_type=model_type_flag)
|
| 266 |
|
| 267 |
-
#
|
| 268 |
conf_summary = (
|
| 269 |
f'<div style="background-color: #ffffff; color: #1a1a1a; padding: 22px; border-radius: 12px; border: 1px solid #e0e0e0; text-align: left; box-shadow: 0 2px 6px rgba(0,0,0,0.03);">'
|
| 270 |
f'<h3 style="color: #00bfa5; margin-top: 0; margin-bottom: 14px; text-align: center; font-size: 1.3em;">📊 置信度分析 (Confidence Analysis)</h3>'
|
|
@@ -276,7 +261,7 @@ def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_st
|
|
| 276 |
f'</div>'
|
| 277 |
f'<p style="font-size: 0.88em; color: #64748b; line-height: 1.6; margin: 0; text-align: justify;">'
|
| 278 |
f'<b>Note:</b> 注意,该置信度校准为 manuscript 中 “Multi-perspective Interpretability Analysis” 节中的 '
|
| 279 |
-
f'<b>Confidence calibration analysis</b> 获取的拟合参数
|
| 280 |
f'</p>'
|
| 281 |
f'</div>'
|
| 282 |
)
|
|
@@ -285,6 +270,10 @@ def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_st
|
|
| 285 |
grid_filename = "search_results_grid.png"
|
| 286 |
grid_img_path = generate_mol_grid_custom(res_smiles, legends, grid_filename)
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
return (
|
| 289 |
gr.update(visible=True),
|
| 290 |
grid_img_path,
|
|
@@ -294,12 +283,12 @@ def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_st
|
|
| 294 |
"Cosine Similarity": [round(s, 4) for s in res_scores],
|
| 295 |
"Calibrated Probability": [f"{calculate_calibrated_confidence(s, current_smiles_model, model_type=model_type_flag)*100:.2f}%" for s in res_scores]
|
| 296 |
}),
|
| 297 |
-
gr.update(visible=True, value=
|
| 298 |
gr.update(value=conf_summary, visible=True)
|
| 299 |
)
|
| 300 |
|
| 301 |
|
| 302 |
-
# ==================== 4. 界面前端 ====================
|
| 303 |
CSS = """
|
| 304 |
* { font-family: 'Times New Roman', Times, serif !important; }
|
| 305 |
body { background-color: #ffffff !important; color: #333333 !important; }
|
|
|
|
| 9 |
from rdkit.Chem.Draw import rdMolDraw2D
|
| 10 |
from huggingface_hub import hf_hub_download
|
| 11 |
|
| 12 |
+
# 导入网络模型结构
|
| 13 |
from IR_encoder import IRModel
|
| 14 |
from SMILES_encoder import SmilesModel
|
| 15 |
+
|
| 16 |
+
# 导入指定的统一网格预处理模块
|
| 17 |
from ir_process import (
|
| 18 |
preprocess_jdx_spectra_higer_500,
|
| 19 |
+
preprocess_csv_spectra_higer_500
|
|
|
|
|
|
|
| 20 |
)
|
| 21 |
|
| 22 |
# ==================== 全局配置 ====================
|
|
|
|
| 117 |
return model_instance
|
| 118 |
|
| 119 |
|
| 120 |
+
# ==================== 1. 初始化双分支检索模型 ====================
|
| 121 |
print("Initializing Models and Feature Libraries...")
|
| 122 |
|
| 123 |
+
# 通用分支 (NIST / Custom 库)
|
| 124 |
ir_model_general = load_model_instance(IRModel(), "overall_best_top1_ir.pth", DEVICE)
|
| 125 |
smiles_model_general = load_model_instance(
|
| 126 |
SmilesModel(roberta_model_path=None, roberta_tokenizer_path=TOKENIZER_PATH, smiles_maxlen=300, feature_dim=768),
|
|
|
|
| 128 |
DEVICE
|
| 129 |
)
|
| 130 |
|
| 131 |
+
# 专属 PS 分支 (PS Existing / PS Derivative 库)
|
| 132 |
ir_model_ps = load_model_instance(IRModel(), "overall_best_nps_recall10_ir.pth", DEVICE)
|
| 133 |
smiles_model_ps = load_model_instance(
|
| 134 |
SmilesModel(roberta_model_path=None, roberta_tokenizer_path=TOKENIZER_PATH, smiles_maxlen=300, feature_dim=768),
|
|
|
|
| 136 |
DEVICE
|
| 137 |
)
|
| 138 |
|
| 139 |
+
# 载入分子特征库
|
| 140 |
library_nist_features = load_gz_features("library_NIST_smiles_features_fp16.gz", DEVICE)
|
| 141 |
general_smiles_nist = load_smiles_list("NIST_smiles_can.txt")
|
| 142 |
|
|
|
|
| 153 |
}
|
| 154 |
|
| 155 |
|
| 156 |
+
# ==================== 2. 置信度计算与分子编码 ====================
|
| 157 |
def calculate_calibrated_confidence(cosine_sim: float, smiles_model_instance, model_type: str = "general") -> float:
|
|
|
|
|
|
|
|
|
|
| 158 |
with torch.no_grad():
|
| 159 |
t = torch.exp(smiles_model_instance.t_prime).cpu().item()
|
| 160 |
b = smiles_model_instance.bias.cpu().item()
|
| 161 |
+
|
|
|
|
| 162 |
raw_logit = cosine_sim * t + b
|
|
|
|
|
|
|
| 163 |
params = PLATT_CALIBRATION_PARAMS.get(model_type, PLATT_CALIBRATION_PARAMS["general"])
|
| 164 |
A = params["A"]
|
| 165 |
B = params["B"]
|
| 166 |
+
|
|
|
|
| 167 |
calibrated_logit = A * raw_logit + B
|
| 168 |
confidence = 1.0 / (1.0 + np.exp(-calibrated_logit))
|
| 169 |
return float(np.clip(confidence, 0.0, 1.0))
|
|
|
|
| 180 |
original_map[can] = s
|
| 181 |
if not valid_smiles:
|
| 182 |
return None, [], []
|
| 183 |
+
|
| 184 |
tokenizer = smiles_model_instance.smiles_tokenizer
|
| 185 |
encoded = [tokenizer(s, max_length=300, padding='max_length', truncation=True, return_tensors='pt') for s in valid_smiles]
|
| 186 |
ids = torch.cat([item['input_ids'] for item in encoded], dim=0).to(DEVICE)
|
|
|
|
| 190 |
return feats, valid_smiles, [original_map[s] for s in valid_smiles]
|
| 191 |
|
| 192 |
|
| 193 |
+
# ==================== 3. 检索主业务流程 ====================
|
| 194 |
def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_str, access_key, progress=gr.Progress()):
|
| 195 |
if library_name == "PS Derivative Library":
|
| 196 |
if access_key != ADMIN_KEY:
|
|
|
|
| 199 |
if ir_file is None:
|
| 200 |
raise gr.Error("Please upload an IR spectrum file.")
|
| 201 |
|
| 202 |
+
# 动态切换专属权重
|
| 203 |
is_ps_lib = library_name in ["PS Existing Library", "PS Derivative Library"]
|
| 204 |
current_ir_model = ir_model_ps if is_ps_lib else ir_model_general
|
| 205 |
current_smiles_model = smiles_model_ps if is_ps_lib else smiles_model_general
|
|
|
|
| 211 |
if file_path.lower().endswith('.csv'):
|
| 212 |
try:
|
| 213 |
df = pd.read_csv(file_path, header=None)
|
| 214 |
+
w_raw, i_raw = df.iloc[:, 0].values, df.iloc[:, 1].values
|
| 215 |
+
ir_data = preprocess_csv_spectra_higer_500(w_raw, i_raw)
|
|
|
|
|
|
|
|
|
|
| 216 |
except Exception as e:
|
| 217 |
raise gr.Error(f"Error processing CSV: {str(e)}")
|
| 218 |
|
| 219 |
elif file_path.lower().endswith('.jdx'):
|
| 220 |
try:
|
| 221 |
data = jcamp.jcamp_readfile(file_path)
|
| 222 |
+
w_raw, i_raw = np.array(data['x']), np.array(data['y'])
|
| 223 |
+
ir_data = preprocess_jdx_spectra_higer_500(w_raw, i_raw)
|
|
|
|
|
|
|
|
|
|
| 224 |
except Exception as e:
|
| 225 |
raise gr.Error(f"Error processing JDX: {str(e)}")
|
| 226 |
|
| 227 |
if ir_data is None:
|
| 228 |
+
raise gr.Error("Unsupported file format or invalid data points. Please check your spectrum file.")
|
| 229 |
|
| 230 |
+
# 光谱编码与检索
|
| 231 |
ir_tensor = torch.tensor(ir_data, dtype=torch.float32).unsqueeze(0).to(DEVICE)
|
| 232 |
with torch.no_grad():
|
| 233 |
ir_feat = current_ir_model(ir_tensor).to(torch.float32)
|
| 234 |
|
|
|
|
| 235 |
if library_name == "Custom Library":
|
| 236 |
lib_features, lib_smiles, display_smiles = process_custom_smiles(custom_smiles_str, current_smiles_model)
|
| 237 |
if lib_features is None:
|
|
|
|
| 240 |
lib_features, lib_smiles = ALL_LIBS[library_name]
|
| 241 |
display_smiles = lib_smiles
|
| 242 |
|
|
|
|
| 243 |
sims = ir_feat @ lib_features.to(DEVICE).to(torch.float32).t()
|
| 244 |
scores, indices = sims.topk(min(10, lib_features.shape[0]), dim=1)
|
| 245 |
res_smiles = [display_smiles[idx.item()] for idx in indices[0]]
|
| 246 |
res_scores = [sco.item() for sco in scores[0]]
|
| 247 |
|
| 248 |
+
# 置信度分析
|
| 249 |
top1_score = res_scores[0]
|
| 250 |
top1_calibrated_conf = calculate_calibrated_confidence(top1_score, current_smiles_model, model_type=model_type_flag)
|
| 251 |
|
| 252 |
+
# 规范置信度卡片展示
|
| 253 |
conf_summary = (
|
| 254 |
f'<div style="background-color: #ffffff; color: #1a1a1a; padding: 22px; border-radius: 12px; border: 1px solid #e0e0e0; text-align: left; box-shadow: 0 2px 6px rgba(0,0,0,0.03);">'
|
| 255 |
f'<h3 style="color: #00bfa5; margin-top: 0; margin-bottom: 14px; text-align: center; font-size: 1.3em;">📊 置信度分析 (Confidence Analysis)</h3>'
|
|
|
|
| 261 |
f'</div>'
|
| 262 |
f'<p style="font-size: 0.88em; color: #64748b; line-height: 1.6; margin: 0; text-align: justify;">'
|
| 263 |
f'<b>Note:</b> 注意,该置信度校准为 manuscript 中 “Multi-perspective Interpretability Analysis” 节中的 '
|
| 264 |
+
f'<b>Confidence calibration analysis</b> 获取的拟合参数,鉴于受到搜索库大小与搜索对象的差异,置信度可能发生偏差,因此,建议根据给出的前置候选物进行综合人工考量。'
|
| 265 |
f'</p>'
|
| 266 |
f'</div>'
|
| 267 |
)
|
|
|
|
| 270 |
grid_filename = "search_results_grid.png"
|
| 271 |
grid_img_path = generate_mol_grid_custom(res_smiles, legends, grid_filename)
|
| 272 |
|
| 273 |
+
# 归一化绘图数据 (4000 -> 500)
|
| 274 |
+
wavenumbers_plot = np.linspace(4000, 500, 3500)
|
| 275 |
+
plot_df = pd.DataFrame({'wavenumber': wavenumbers_plot, 'intensity': ir_data})
|
| 276 |
+
|
| 277 |
return (
|
| 278 |
gr.update(visible=True),
|
| 279 |
grid_img_path,
|
|
|
|
| 283 |
"Cosine Similarity": [round(s, 4) for s in res_scores],
|
| 284 |
"Calibrated Probability": [f"{calculate_calibrated_confidence(s, current_smiles_model, model_type=model_type_flag)*100:.2f}%" for s in res_scores]
|
| 285 |
}),
|
| 286 |
+
gr.update(visible=True, value=plot_df),
|
| 287 |
gr.update(value=conf_summary, visible=True)
|
| 288 |
)
|
| 289 |
|
| 290 |
|
| 291 |
+
# ==================== 4. 界面前端构建 ====================
|
| 292 |
CSS = """
|
| 293 |
* { font-family: 'Times New Roman', Times, serif !important; }
|
| 294 |
body { background-color: #ffffff !important; color: #333333 !important; }
|