Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -25,11 +25,16 @@ TOKENIZER_PATH = r"tokenizer-smiles-roberta-1e_new"
|
|
| 25 |
EXAMPLE_DIR = "example_files"
|
| 26 |
RETRIEVAL_WEIGHTS_DIR = r"model_weights"
|
| 27 |
PROCESSED_DATA_DIR = r"processed_data"
|
|
|
|
| 28 |
|
|
|
|
| 29 |
ADMIN_KEY = os.environ.get("ADMIN_KEY", "123456")
|
| 30 |
-
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 31 |
PRIVATE_DATASET_REPO = "Hsqcsu/Derivative_PS_data"
|
| 32 |
|
|
|
|
|
|
|
|
|
|
| 33 |
# ==================== Platt Calibration 拟合参数 ====================
|
| 34 |
PLATT_CALIBRATION_PARAMS = {
|
| 35 |
"general": {
|
|
@@ -84,18 +89,20 @@ def download_private_file(filename):
|
|
| 84 |
repo_id=PRIVATE_DATASET_REPO,
|
| 85 |
filename=filename,
|
| 86 |
repo_type="dataset",
|
| 87 |
-
token=HF_TOKEN,
|
| 88 |
local_dir=PROCESSED_DATA_DIR
|
| 89 |
)
|
| 90 |
return path
|
| 91 |
except Exception as e:
|
| 92 |
-
print(f"
|
| 93 |
-
return
|
| 94 |
return local_path
|
| 95 |
|
| 96 |
|
| 97 |
def load_gz_features(filename, device):
|
| 98 |
path = download_private_file(filename)
|
|
|
|
|
|
|
| 99 |
with gzip.open(path, 'rb') as f:
|
| 100 |
features = torch.load(f, weights_only=False)
|
| 101 |
return features.to(device)
|
|
@@ -103,6 +110,8 @@ def load_gz_features(filename, device):
|
|
| 103 |
|
| 104 |
def load_smiles_list(filename):
|
| 105 |
path = download_private_file(filename)
|
|
|
|
|
|
|
| 106 |
with open(path, 'r', encoding='utf-8') as f:
|
| 107 |
return [line.strip() for line in f if line.strip()]
|
| 108 |
|
|
@@ -117,10 +126,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 +137,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,24 +145,46 @@ 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 |
|
| 143 |
-
library_derivative_ps_features = load_gz_features("library_Derivative_PS_smiles_features_fp16.gz", DEVICE)
|
| 144 |
-
ps_smiles_derivative = load_smiles_list("smiles_Derivative_PS.txt")
|
| 145 |
-
|
| 146 |
library_existed_ps_features = load_gz_features("library_Existing_PS_smiles_features_fp16.gz", DEVICE)
|
| 147 |
ps_smiles_existed = load_smiles_list("smiles_Existing_PS.txt")
|
| 148 |
|
| 149 |
-
|
| 150 |
"NIST": (library_nist_features, general_smiles_nist),
|
| 151 |
-
"PS Existing Library": (library_existed_ps_features, ps_smiles_existed)
|
| 152 |
-
"PS Derivative Library": (library_derivative_ps_features, ps_smiles_derivative)
|
| 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()
|
|
@@ -190,14 +221,10 @@ def process_custom_smiles(smiles_str, smiles_model_instance):
|
|
| 190 |
return feats, valid_smiles, [original_map[s] for s in valid_smiles]
|
| 191 |
|
| 192 |
|
| 193 |
-
# ====================
|
| 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:
|
| 197 |
-
raise gr.Error("⛔ Access Denied: Incorrect Access Key for PS Derivative Library.")
|
| 198 |
-
|
| 199 |
if ir_file is None:
|
| 200 |
-
raise gr.Error("
|
| 201 |
|
| 202 |
# 动态切换专属权重
|
| 203 |
is_ps_lib = library_name in ["PS Existing Library", "PS Derivative Library"]
|
|
@@ -214,7 +241,7 @@ def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_st
|
|
| 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"
|
| 218 |
|
| 219 |
elif file_path.lower().endswith('.jdx'):
|
| 220 |
try:
|
|
@@ -222,24 +249,28 @@ def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_st
|
|
| 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"
|
| 226 |
|
| 227 |
if ir_data is None:
|
| 228 |
-
raise gr.Error("
|
| 229 |
|
| 230 |
-
#
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 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:
|
| 238 |
-
raise gr.Error("
|
| 239 |
else:
|
| 240 |
-
lib_features, lib_smiles =
|
| 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]]
|
|
@@ -270,7 +301,6 @@ def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_st
|
|
| 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 |
|
|
@@ -288,7 +318,7 @@ def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_st
|
|
| 288 |
)
|
| 289 |
|
| 290 |
|
| 291 |
-
# ====================
|
| 292 |
CSS = """
|
| 293 |
* { font-family: 'Times New Roman', Times, serif !important; }
|
| 294 |
body { background-color: #ffffff !important; color: #333333 !important; }
|
|
|
|
| 25 |
EXAMPLE_DIR = "example_files"
|
| 26 |
RETRIEVAL_WEIGHTS_DIR = r"model_weights"
|
| 27 |
PROCESSED_DATA_DIR = r"processed_data"
|
| 28 |
+
os.makedirs(PROCESSED_DATA_DIR, exist_ok=True)
|
| 29 |
|
| 30 |
+
# 密钥与私有仓库配置 (仅服务端可见)
|
| 31 |
ADMIN_KEY = os.environ.get("ADMIN_KEY", "123456")
|
| 32 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", None)
|
| 33 |
PRIVATE_DATASET_REPO = "Hsqcsu/Derivative_PS_data"
|
| 34 |
|
| 35 |
+
# 衍生库内存缓存 (启动时保持为 None,只有密码正确时才按需加载)
|
| 36 |
+
DERIVATIVE_PS_CACHE = None
|
| 37 |
+
|
| 38 |
# ==================== Platt Calibration 拟合参数 ====================
|
| 39 |
PLATT_CALIBRATION_PARAMS = {
|
| 40 |
"general": {
|
|
|
|
| 89 |
repo_id=PRIVATE_DATASET_REPO,
|
| 90 |
filename=filename,
|
| 91 |
repo_type="dataset",
|
| 92 |
+
token=HF_TOKEN if HF_TOKEN else True,
|
| 93 |
local_dir=PROCESSED_DATA_DIR
|
| 94 |
)
|
| 95 |
return path
|
| 96 |
except Exception as e:
|
| 97 |
+
print(f"[Auth Error] Failed to download {filename}: {e}")
|
| 98 |
+
return None
|
| 99 |
return local_path
|
| 100 |
|
| 101 |
|
| 102 |
def load_gz_features(filename, device):
|
| 103 |
path = download_private_file(filename)
|
| 104 |
+
if path is None or not os.path.exists(path):
|
| 105 |
+
return None
|
| 106 |
with gzip.open(path, 'rb') as f:
|
| 107 |
features = torch.load(f, weights_only=False)
|
| 108 |
return features.to(device)
|
|
|
|
| 110 |
|
| 111 |
def load_smiles_list(filename):
|
| 112 |
path = download_private_file(filename)
|
| 113 |
+
if path is None or not os.path.exists(path):
|
| 114 |
+
return []
|
| 115 |
with open(path, 'r', encoding='utf-8') as f:
|
| 116 |
return [line.strip() for line in f if line.strip()]
|
| 117 |
|
|
|
|
| 126 |
return model_instance
|
| 127 |
|
| 128 |
|
| 129 |
+
# ==================== 1. 初始化模型与基础库 (启动时不加载衍生库) ====================
|
| 130 |
+
print("Initializing Models and Base Feature Libraries...")
|
| 131 |
|
| 132 |
+
# 通用分支模型
|
| 133 |
ir_model_general = load_model_instance(IRModel(), "overall_best_top1_ir.pth", DEVICE)
|
| 134 |
smiles_model_general = load_model_instance(
|
| 135 |
SmilesModel(roberta_model_path=None, roberta_tokenizer_path=TOKENIZER_PATH, smiles_maxlen=300, feature_dim=768),
|
|
|
|
| 137 |
DEVICE
|
| 138 |
)
|
| 139 |
|
| 140 |
+
# 专属 PS 分支模型
|
| 141 |
ir_model_ps = load_model_instance(IRModel(), "overall_best_nps_recall10_ir.pth", DEVICE)
|
| 142 |
smiles_model_ps = load_model_instance(
|
| 143 |
SmilesModel(roberta_model_path=None, roberta_tokenizer_path=TOKENIZER_PATH, smiles_maxlen=300, feature_dim=768),
|
|
|
|
| 145 |
DEVICE
|
| 146 |
)
|
| 147 |
|
| 148 |
+
# 仅加载公开库
|
| 149 |
library_nist_features = load_gz_features("library_NIST_smiles_features_fp16.gz", DEVICE)
|
| 150 |
general_smiles_nist = load_smiles_list("NIST_smiles_can.txt")
|
| 151 |
|
|
|
|
|
|
|
|
|
|
| 152 |
library_existed_ps_features = load_gz_features("library_Existing_PS_smiles_features_fp16.gz", DEVICE)
|
| 153 |
ps_smiles_existed = load_smiles_list("smiles_Existing_PS.txt")
|
| 154 |
|
| 155 |
+
STATIC_LIBS = {
|
| 156 |
"NIST": (library_nist_features, general_smiles_nist),
|
| 157 |
+
"PS Existing Library": (library_existed_ps_features, ps_smiles_existed)
|
|
|
|
| 158 |
}
|
| 159 |
|
| 160 |
|
| 161 |
+
# ==================== 2. 衍生库动态按需鉴权加载 ====================
|
| 162 |
+
def get_authorized_derivative_ps_library(access_key):
|
| 163 |
+
"""
|
| 164 |
+
只有输入正确密钥时,才动态向私有仓库请求并加载衍生库数据
|
| 165 |
+
"""
|
| 166 |
+
global DERIVATIVE_PS_CACHE
|
| 167 |
+
|
| 168 |
+
if not access_key or access_key.strip() != ADMIN_KEY:
|
| 169 |
+
raise gr.Error("⛔ 访问被拒绝:请输入正确的 PS 衍生库访问密钥 (Access Key)。")
|
| 170 |
+
|
| 171 |
+
# 若已授权且已加载过,直接返回内存缓存
|
| 172 |
+
if DERIVATIVE_PS_CACHE is not None:
|
| 173 |
+
return DERIVATIVE_PS_CACHE
|
| 174 |
+
|
| 175 |
+
# 首次输入正确密码时触发安全下载与加载
|
| 176 |
+
print("--> 密钥验证成功,正在动态加载 PS 衍生库特征...")
|
| 177 |
+
feat = load_gz_features("library_Derivative_PS_smiles_features_fp16.gz", DEVICE)
|
| 178 |
+
smiles = load_smiles_list("smiles_Derivative_PS.txt")
|
| 179 |
+
|
| 180 |
+
if feat is None or len(smiles) == 0:
|
| 181 |
+
raise gr.Error("⛔ 衍生库加载失败:请检查 Hugging Face Space Settings 中是否已配置有效的 HF_TOKEN。")
|
| 182 |
+
|
| 183 |
+
DERIVATIVE_PS_CACHE = (feat, smiles)
|
| 184 |
+
return DERIVATIVE_PS_CACHE
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
# ==================== 3. 置信度计算与分子编码 ====================
|
| 188 |
def calculate_calibrated_confidence(cosine_sim: float, smiles_model_instance, model_type: str = "general") -> float:
|
| 189 |
with torch.no_grad():
|
| 190 |
t = torch.exp(smiles_model_instance.t_prime).cpu().item()
|
|
|
|
| 221 |
return feats, valid_smiles, [original_map[s] for s in valid_smiles]
|
| 222 |
|
| 223 |
|
| 224 |
+
# ==================== 4. 检索主业务流程 ====================
|
| 225 |
def run_unified_retrieval(ir_file, spectrum_type, library_name, custom_smiles_str, access_key, progress=gr.Progress()):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
if ir_file is None:
|
| 227 |
+
raise gr.Error("请上传待检索的红外光谱文件。")
|
| 228 |
|
| 229 |
# 动态切换专属权重
|
| 230 |
is_ps_lib = library_name in ["PS Existing Library", "PS Derivative Library"]
|
|
|
|
| 241 |
w_raw, i_raw = df.iloc[:, 0].values, df.iloc[:, 1].values
|
| 242 |
ir_data = preprocess_csv_spectra_higer_500(w_raw, i_raw)
|
| 243 |
except Exception as e:
|
| 244 |
+
raise gr.Error(f"CSV 解析处理失败: {str(e)}")
|
| 245 |
|
| 246 |
elif file_path.lower().endswith('.jdx'):
|
| 247 |
try:
|
|
|
|
| 249 |
w_raw, i_raw = np.array(data['x']), np.array(data['y'])
|
| 250 |
ir_data = preprocess_jdx_spectra_higer_500(w_raw, i_raw)
|
| 251 |
except Exception as e:
|
| 252 |
+
raise gr.Error(f"JDX 解析处理失败: {str(e)}")
|
| 253 |
|
| 254 |
if ir_data is None:
|
| 255 |
+
raise gr.Error("文件格式不支持或光谱数据点无效,请检查文件。")
|
| 256 |
|
| 257 |
+
# 获取匹配目标库 (针对衍生库执行强密码鉴权)
|
| 258 |
+
if library_name == "PS Derivative Library":
|
| 259 |
+
lib_features, lib_smiles = get_authorized_derivative_ps_library(access_key)
|
| 260 |
+
display_smiles = lib_smiles
|
| 261 |
+
elif library_name == "Custom Library":
|
|
|
|
| 262 |
lib_features, lib_smiles, display_smiles = process_custom_smiles(custom_smiles_str, current_smiles_model)
|
| 263 |
if lib_features is None:
|
| 264 |
+
raise gr.Error("未提供有效的自定义 SMILES 列表。")
|
| 265 |
else:
|
| 266 |
+
lib_features, lib_smiles = STATIC_LIBS[library_name]
|
| 267 |
display_smiles = lib_smiles
|
| 268 |
|
| 269 |
+
# 光谱编码与向量检索
|
| 270 |
+
ir_tensor = torch.tensor(ir_data, dtype=torch.float32).unsqueeze(0).to(DEVICE)
|
| 271 |
+
with torch.no_grad():
|
| 272 |
+
ir_feat = current_ir_model(ir_tensor).to(torch.float32)
|
| 273 |
+
|
| 274 |
sims = ir_feat @ lib_features.to(DEVICE).to(torch.float32).t()
|
| 275 |
scores, indices = sims.topk(min(10, lib_features.shape[0]), dim=1)
|
| 276 |
res_smiles = [display_smiles[idx.item()] for idx in indices[0]]
|
|
|
|
| 301 |
grid_filename = "search_results_grid.png"
|
| 302 |
grid_img_path = generate_mol_grid_custom(res_smiles, legends, grid_filename)
|
| 303 |
|
|
|
|
| 304 |
wavenumbers_plot = np.linspace(4000, 500, 3500)
|
| 305 |
plot_df = pd.DataFrame({'wavenumber': wavenumbers_plot, 'intensity': ir_data})
|
| 306 |
|
|
|
|
| 318 |
)
|
| 319 |
|
| 320 |
|
| 321 |
+
# ==================== 5. 前端界面 ====================
|
| 322 |
CSS = """
|
| 323 |
* { font-family: 'Times New Roman', Times, serif !important; }
|
| 324 |
body { background-color: #ffffff !important; color: #333333 !important; }
|