msformulator 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ """msformulator - 基于扩散模型的质谱分子式预测包。
2
+
3
+ 入口函数:main(pr_mz, mz2, type_str, peak_type_str)
4
+ pr_mz : 母离子精确质量 (float)
5
+ mz2 : 二级碎片峰列表,形如 [[mz, intensity], ...]
6
+ type_str : 母离子加合类型,如 "[M+H]+"
7
+ peak_type_str: 碎片峰加合类型,如 "[M+H]+"
8
+
9
+ 返回候选分子式列表(按 ppm 误差升序)。
10
+ """
11
+
12
+ from .main import main
13
+ from .model import MSFormulator, GaussianDiffusion
14
+
15
+ __all__ = ["main", "MSFormulator", "GaussianDiffusion"]
@@ -0,0 +1,627 @@
1
+ """chemistry.py - 分子式相关的化学 / 质量计算工具函数。
2
+
3
+ 从原始 main.py 抽取,作为 msformulator 包的纯计算模块(不依赖 torch)。
4
+ 包含:质量微调 (tiaozheng_f / tiaozheng_f2)、不饱和度校验、ppm 计算、
5
+ 分子质量计算、分子式各元素数量的正则提取、离子模式解析等。
6
+ """
7
+
8
+ import re
9
+ import numpy as np
10
+ from molmass import Formula
11
+
12
+
13
+ def tiaozheng_f2(pr_mass, f_mass,
14
+ c_num, o_num, n_num,
15
+ cl_num, s_num, p_num,
16
+ f_num, h_num):
17
+
18
+ # 单同位素质量常量
19
+ MASS = (
20
+ 12.0, # C
21
+ 15.99491461957, # O
22
+ 14.00307400443, # N
23
+ 34.968852682, # Cl
24
+ 31.9720711744, # S
25
+ 30.97376199842, # P
26
+ 18.99840316273, # F
27
+ 1.00782503223 # H
28
+ )
29
+
30
+ max_iter = 20 # 防止极端死循环
31
+ iteration = 0
32
+
33
+ while iteration < max_iter:
34
+ iteration += 1
35
+
36
+ mass_diff = pr_mass - f_mass
37
+
38
+ abs_mass = abs(mass_diff)
39
+
40
+ changed = False
41
+ if c_num == 25 and n_num == 1 and o_num == 8 and h_num == 47:
42
+ print( pr_mass - f_mass)
43
+ print("--------------------")
44
+
45
+ # ===========================
46
+ # 原逻辑开始
47
+ # ===========================
48
+
49
+ if 59 < abs_mass < 63 and p_num == 1:
50
+ p_num += -2 if mass_diff < 0 else 2
51
+ changed = True
52
+
53
+ elif 11.9 < abs_mass < 13.0:
54
+ c_num += -1 if mass_diff < 0 else 1
55
+ changed = True
56
+
57
+ elif abs_mass > 50:
58
+ break
59
+
60
+ elif 29.8 < abs_mass < 31 and p_num == 1:
61
+ p_num += -1 if mass_diff < 0 else 1
62
+ changed = True
63
+
64
+ elif 10 < abs_mass < 29 and h_num == 100:
65
+ c_h = int(abs_mass / 1.008) + 1
66
+ h_num += -c_h if mass_diff < 0 else c_h
67
+ changed = True
68
+
69
+ elif abs_mass > 1 and int(abs_mass) % 12 == 0 and (
70
+ (mass_diff < 0 and int(abs_mass / 12) < c_num) or mass_diff > 0
71
+ ):
72
+ c_c = int(abs_mass / 12)
73
+ if mass_diff < 0 and c_num > c_c:
74
+ c_num -= c_c
75
+ if mass_diff > 0:
76
+ c_num += c_c
77
+ changed = True
78
+
79
+ elif 15.9 < abs_mass < 16.01 and o_num >= 0:
80
+
81
+ o_num += -1 if mass_diff < 0 else 1
82
+ changed = True
83
+ elif 1 < abs_mass < 20 and h_num > 1:
84
+ h_num += -1 if mass_diff < 0 else 1
85
+
86
+ changed = True
87
+
88
+
89
+ else:
90
+ break
91
+
92
+ if not changed:
93
+ break
94
+
95
+ # ===========================
96
+ # 手算新质量(超快)
97
+ # ===========================
98
+
99
+ f_mass = (
100
+ c_num * MASS[0] +
101
+ o_num * MASS[1] +
102
+ n_num * MASS[2] +
103
+ cl_num * MASS[3] +
104
+ s_num * MASS[4] +
105
+ p_num * MASS[5] +
106
+ f_num * MASS[6] +
107
+ h_num * MASS[7]
108
+ )
109
+
110
+ # ppm 判断
111
+ ppm = abs((pr_mass - f_mass) / f_mass * 1e6)
112
+
113
+ if ppm < 20:
114
+ break
115
+
116
+
117
+ return c_num, o_num, n_num, cl_num, s_num, p_num, f_num, h_num
118
+
119
+ def tiaozheng_f(pr_mass,f_mass,f):
120
+ mass=pr_mass-f_mass
121
+
122
+ c_num = extract_c_numbers(f)[0]
123
+ o_num = extract_o_numbers(f)
124
+
125
+ n_num = extract_n_numbers(f)
126
+
127
+ f_num = extract_f_numbers(f)
128
+
129
+
130
+ s_num = extract_s_numbers(f)
131
+
132
+
133
+ p_num = extract_p_numbers(f)
134
+
135
+
136
+ cl_num = extract_cl_numbers(f)
137
+
138
+ h_num = extract_h_numbers(f)
139
+ # print(c_num, o_num, n_num, cl_num, s_num, p_num, f_num, h_num)
140
+ if np.abs(mass)>50:
141
+ return f
142
+ elif np.abs(mass)>29 and np.abs(mass)<31:
143
+ if mass<0:
144
+ p_num = p_num - 1
145
+ else:
146
+ p_num = p_num + 1
147
+ elif np.abs(mass)>11.5 and np.abs(mass)<12.5:
148
+
149
+ if mass<0:
150
+
151
+ c_num = c_num - 1
152
+
153
+ else:
154
+ c_num = c_num + 1
155
+
156
+
157
+ elif np.abs(mass)>59 and np.abs(mass)<63 and p_num==1:
158
+ if mass<0:
159
+ p_num = p_num - 2
160
+ else:
161
+ p_num = p_num + 2
162
+ # elif np.abs(mass)>10 and h_num==49:
163
+ # c_h=(int(np.abs(mass)/1.007)+1)
164
+ # if mass<0:
165
+ # h_num = h_num - c_h
166
+ # else:
167
+ # h_num = h_num + c_h
168
+ # elif (int(np.abs(mass))%12)==0 :
169
+ # c_h=(int(np.abs(mass)/12))
170
+ # if mass<0:
171
+ # c_num = c_num - c_h
172
+ # else:
173
+ # c_num = c_num + c_h
174
+ elif np.abs(mass)>31.5 and np.abs(mass)<33 :
175
+ c_h=(int(np.abs(mass)/1.007)+1)
176
+ if mass<0:
177
+ s_num = s_num - 1
178
+ else:
179
+ s_num = s_num + 1
180
+ elif np.abs(mass)>15.5 and np.abs(mass)<17 :
181
+ c_h=(int(np.abs(mass)/1.007)+1)
182
+ if mass<0:
183
+ o_num = o_num - 1
184
+ else:
185
+ o_num = o_num + 1
186
+ elif np.abs(mass)>1 and np.abs(mass)<11 :
187
+
188
+ if mass < 0:
189
+
190
+ h_num = h_num - 1
191
+
192
+ else:
193
+ h_num = h_num + 1
194
+ else:
195
+ return f
196
+ new_f = ""
197
+
198
+ new_f += "C" + str(c_num)
199
+ if h_num > 0:
200
+ new_f += "H" + str(h_num)
201
+ if cl_num > 0:
202
+ new_f += "Cl" + str(cl_num)
203
+ if n_num > 0:
204
+ new_f += "N" + str(n_num)
205
+ if o_num > 0:
206
+ new_f += "O" + str(o_num)
207
+
208
+ if s_num > 0:
209
+ new_f += "S" + str(s_num)
210
+ if p_num > 0:
211
+ new_f += "P" + str(p_num)
212
+ if f_num > 0:
213
+ new_f += "F" + str(f_num)
214
+ new_mass=calculate_molecular_mass(new_f)['mass']
215
+ if calculate_ppm(pr_mass,new_mass,use_absolute=True)<20:
216
+
217
+ return new_f
218
+ else:
219
+ return tiaozheng_f(pr_mass,new_mass,new_f)
220
+
221
+ def calculate_unsaturation_and_check(C, O, N, Cl, S, P, F, H, allow_half_integer=False):
222
+ """
223
+ 计算分子的不饱和度并判断是否合理
224
+
225
+ 参数:
226
+ C, O, N, Cl, S, P, F, H: 各原子的数量(整数)
227
+ allow_half_integer: 是否允许半整数不饱和度(对于离子/自由基),默认False
228
+
229
+ 返回:
230
+ (不饱和度, 是否合理): 返回一个元组,包含计算得到的不饱和度和判断结果(1/0)
231
+ """
232
+
233
+ # 计算不饱和度
234
+ # 公式: Ω = (2C + 2 + N - H - 卤素总数) / 2
235
+ # 其中卤素包括F, Cl, Br, I。这里我们有F和Cl
236
+ halogens = F + Cl
237
+ numerator = 2 * C + 2 + N - H - halogens
238
+ unsaturation = numerator / 2.0
239
+
240
+ # 判断是否合理
241
+ is_reasonable = 1 # 初始假设合理
242
+
243
+ # 条件0: 分子必须有至少一个碳原子(对有机分子而言)
244
+ if C <= 0:
245
+ is_reasonable = 0
246
+ return unsaturation, is_reasonable
247
+
248
+ # 条件1: 不饱和度必须是非负数
249
+ if unsaturation < 0:
250
+ is_reasonable = 0
251
+ return unsaturation, is_reasonable
252
+
253
+ # 条件2: 检查不饱和度是否为整数
254
+ # 如果不允许半整数,则必须为整数
255
+ if not allow_half_integer:
256
+ if not float(unsaturation).is_integer():
257
+ is_reasonable = 0
258
+ return unsaturation, is_reasonable
259
+ else:
260
+ # 如果允许半整数,检查2*Ω是否为整数
261
+ if not float(2 * unsaturation).is_integer():
262
+ is_reasonable = 0
263
+ return unsaturation, is_reasonable
264
+
265
+ # 条件3: 计算理论最大不饱和度
266
+ # 当H=0时的不饱和度
267
+ max_unsaturation = (2 * C + 2 + N - halogens) / 2.0
268
+ if unsaturation > max_unsaturation:
269
+ is_reasonable = 0
270
+ return unsaturation, is_reasonable
271
+
272
+ # 条件4: 所有原子数必须非负
273
+ atom_counts = [C, O, N, Cl, S, P, F, H]
274
+ for count in atom_counts:
275
+ if count < 0:
276
+ is_reasonable = 0
277
+ return unsaturation, is_reasonable
278
+
279
+ # 条件5: 检查氢原子数的奇偶性
280
+ # 对于中性分子,当氮原子数为奇数时,氢原子数应为奇数
281
+ # 当氮原子数为偶数时,氢原子数应为偶数
282
+ if N % 2 == 1: # 氮原子数为奇数
283
+ if H % 2 == 0: # 氢原子数为偶数
284
+ is_reasonable = 0
285
+ elif N!=0: # 氮原子数为偶数
286
+ if H % 2 == 1: # 氢原子数为奇数
287
+ is_reasonable = 0
288
+
289
+ return unsaturation, is_reasonable
290
+
291
+ def check_molecular_formula(C, O, N, Cl, S, P, F, H, allow_half_integer=False):
292
+ """
293
+ 完整的分子式检查函数
294
+
295
+ 参数:
296
+ 各原子数量
297
+ allow_half_integer: 是否允许半整数不饱和度,默认False
298
+
299
+ 返回:
300
+ 合理返回1,不合理返回0
301
+ """
302
+
303
+ unsaturation, is_reasonable = calculate_unsaturation_and_check(C, O, N, Cl, S, P, F, H, allow_half_integer)
304
+ # if C == 9 and H == 14 and N == 3 and O == 7 and P == 1:
305
+ # print(unsaturation, is_reasonable)
306
+ return is_reasonable
307
+
308
+ def calculate_ppm(mass1, mass2, use_absolute=True, use_average=False):
309
+ """
310
+ 计算两个质量之间的ppm差异
311
+
312
+ 参数:
313
+ mass1, mass2: 两个质量值(单位相同)
314
+ use_absolute: 是否返回绝对值(True)或有符号值(False)
315
+ use_average: 是否使用平均值作为分母(False时用mass2作为分母)
316
+
317
+ 返回:
318
+ ppm差异值
319
+ """
320
+ if mass2 == 0:
321
+ raise ValueError("分母不能为0")
322
+
323
+ if use_average:
324
+ # 使用平均值作为分母
325
+ denominator = (mass1 + mass2) / 2.0
326
+ else:
327
+ # 使用mass2作为分母(通常作为理论值或参考值)
328
+ denominator = mass2
329
+
330
+ ppm = (mass1 - mass2) / denominator * 1e6
331
+
332
+ if use_absolute:
333
+ return abs(ppm)
334
+ else:
335
+ return ppm
336
+
337
+ def calculate_molecular_mass(formula, charge=0, ion_type='M'):
338
+ """计算分子质量(推荐使用)"""
339
+ try:
340
+ # 单同位素质量
341
+
342
+ return {
343
+ 'formula': formula,
344
+ 'mass': Formula(formula).isotope.mass,
345
+ }
346
+ except Exception as e:
347
+ raise ValueError(f"计算失败: {e}")
348
+
349
+ def softmax(x, axis=-1):
350
+ """
351
+ Softmax函数实现
352
+
353
+ 参数:
354
+ x: 输入数组
355
+ axis: 沿着哪个轴计算softmax(默认最后一个轴)
356
+
357
+ 返回:
358
+ softmax计算结果
359
+ """
360
+ # 防止数值溢出,减去最大值
361
+ x_max = np.max(x, axis=axis, keepdims=True)
362
+ exp_x = np.exp(x - x_max)
363
+
364
+ return exp_x / np.sum(exp_x, axis=axis, keepdims=True)
365
+
366
+ def extract_h_numbers(s):
367
+ """
368
+ 使用正则表达式在字符串中查找 'H' 及其后的数字。
369
+
370
+ 参数:
371
+ s (str): 待检查的字符串。
372
+
373
+ 返回:
374
+ int:
375
+ - 如果未找到 'H',返回 0
376
+ - 如果找到 'H' 且其后没有数字字符,返回 1
377
+ - 如果找到 'H' 且其后有数字字符,则返回这些连续数字字符组成的整数
378
+ """
379
+ # 模式解释:H 后面捕获一个或多个数字(\d+)
380
+ pattern = r'H(\d+)'
381
+ match = re.search(pattern, s)
382
+ if match:
383
+ # 如果找到 'H' 并且后面有数字,返回匹配到的数字
384
+ return int(match.group(1))
385
+ else:
386
+ # 检查字符串中是否有 'H'(不论后面是否有数字)
387
+ if 'H' in s: # 使用 in 运算符检查字符是否存在[1,2](@ref)
388
+ return 1
389
+ else:
390
+ return 0
391
+
392
+ def extract_s_numbers(s):
393
+ """
394
+ 使用正则表达式在字符串中查找 'H' 及其后的数字。
395
+
396
+ 参数:
397
+ s (str): 待检查的字符串。
398
+
399
+ 返回:
400
+ int:
401
+ - 如果未找到 'H',返回 0
402
+ - 如果找到 'H' 且其后没有数字字符,返回 1
403
+ - 如果找到 'H' 且其后有数字字符,则返回这些连续数字字符组成的整数
404
+ """
405
+ # 模式解释:H 后面捕获一个或多个数字(\d+)
406
+ pattern = r'S(\d+)'
407
+ match = re.search(pattern, s)
408
+ if match:
409
+ # 如果找到 'H' 并且后面有数字,返回匹配到的数字
410
+ return int(match.group(1))
411
+ else:
412
+ # 检查字符串中是否有 'H'(不论后面是否有数字)
413
+ if 'S' in s: # 使用 in 运算符检查字符是否存在[1,2](@ref)
414
+ return 1
415
+ else:
416
+ return 0
417
+
418
+ def extract_p_numbers(s):
419
+ """
420
+ 使用正则表达式在字符串中查找 'H' 及其后的数字。
421
+
422
+ 参数:
423
+ s (str): 待检查的字符串。
424
+
425
+ 返回:
426
+ int:
427
+ - 如果未找到 'H',返回 0
428
+ - 如果找到 'H' 且其后没有数字字符,返回 1
429
+ - 如果找到 'H' 且其后有数字字符,则返回这些连续数字字符组成的整数
430
+ """
431
+ # 模式解释:H 后面捕获一个或多个数字(\d+)
432
+ pattern = r'P(\d+)'
433
+ match = re.search(pattern, s)
434
+ if match:
435
+ # 如果找到 'H' 并且后面有数字,返回匹配到的数字
436
+ return int(match.group(1))
437
+ else:
438
+ # 检查字符串中是否有 'H'(不论后面是否有数字)
439
+ if 'P' in s: # 使用 in 运算符检查字符是否存在[1,2](@ref)
440
+ return 1
441
+ else:
442
+ return 0
443
+
444
+ def extract_f_numbers(s):
445
+ """
446
+ 使用正则表达式在字符串中查找 'H' 及其后的数字。
447
+
448
+ 参数:
449
+ s (str): 待检查的字符串。
450
+
451
+ 返回:
452
+ int:
453
+ - 如果未找到 'H',返回 0
454
+ - 如果找到 'H' 且其后没有数字字符,返回 1
455
+ - 如果找到 'H' 且其后有数字字符,则返回这些连续数字字符组成的整数
456
+ """
457
+ # 模式解释:H 后面捕获一个或多个数字(\d+)
458
+ pattern = r'F(\d+)'
459
+ match = re.search(pattern, s)
460
+ if match:
461
+ # 如果找到 'H' 并且后面有数字,返回匹配到的数字
462
+ return int(match.group(1))
463
+ else:
464
+ # 检查字符串中是否有 'H'(不论后面是否有数字)
465
+ if 'F' in s: # 使用 in 运算符检查字符是否存在[1,2](@ref)
466
+ return 1
467
+ else:
468
+ return 0
469
+
470
+ def extract_cl_numbers(s):
471
+ """
472
+ 使用正则表达式在字符串中查找 'H' 及其后的数字。
473
+
474
+ 参数:
475
+ s (str): 待检查的字符串。
476
+
477
+ 返回:
478
+ int:
479
+ - 如果未找到 'H',返回 0
480
+ - 如果找到 'H' 且其后没有数字字符,返回 1
481
+ - 如果找到 'H' 且其后有数字字符,则返回这些连续数字字符组成的整数
482
+ """
483
+ # 模式解释:H 后面捕获一个或多个数字(\d+)
484
+ pattern = r'Cl(\d+)'
485
+ match = re.search(pattern, s)
486
+ if match:
487
+ # 如果找到 'H' 并且后面有数字,返回匹配到的数字
488
+ return int(match.group(1))
489
+ else:
490
+ # 检查字符串中是否有 'H'(不论后面是否有数字)
491
+ if 'Cl' in s: # 使用 in 运算符检查字符是否存在[1,2](@ref)
492
+ return 1
493
+ else:
494
+ return 0
495
+
496
+ def extract_c_numbers(text):
497
+ pattern = r'C\s*(\d+\.?\d*)' # 模式
498
+ matches = re.findall(pattern, text, re.IGNORECASE) # 全局匹配并忽略大小写
499
+ return [float(num) if '.' in num else int(num) for num in matches] # 转为数字类型
500
+
501
+ def extract_o_numbers(s):
502
+
503
+ """
504
+ 使用正则表达式在字符串中查找 'H' 及其后的数字。
505
+
506
+ 参数:
507
+ s (str): 待检查的字符串。
508
+
509
+ 返回:
510
+ int:
511
+ - 如果未找到 'H',返回 0
512
+ - 如果找到 'H' 且其后没有数字字符,返回 1
513
+ - 如果找到 'H' 且其后有数字字符,则返回这些连续数字字符组成的整数
514
+ """
515
+ # 模式解释:H 后面捕获一个或多个数字(\d+)
516
+ pattern = r'O(\d+)'
517
+ match = re.search(pattern, s)
518
+ if match:
519
+ # 如果找到 'H' 并且后面有数字,返回匹配到的数字
520
+ return int(match.group(1))
521
+ else:
522
+ # 检查字符串中是否有 'H'(不论后面是否有数字)
523
+ if 'O' in s: # 使用 in 运算符检查字符是否存在[1,2](@ref)
524
+ return 1
525
+ else:
526
+ return 0
527
+
528
+ def extract_n_numbers(s):
529
+ """
530
+ 使用正则表达式在字符串中查找 'H' 及其后的数字。
531
+
532
+ 参数:
533
+ s (str): 待检查的字符串。
534
+
535
+ 返回:
536
+ int:
537
+ - 如果未找到 'H',返回 0
538
+ - 如果找到 'H' 且其后没有数字字符,返回 1
539
+ - 如果找到 'H' 且其后有数字字符,则返回这些连续数字字符组成的整数
540
+ """
541
+ # 模式解释:H 后面捕获一个或多个数字(\d+)
542
+ pattern = r'N(\d+)'
543
+ match = re.search(pattern, s)
544
+ if match:
545
+ # 如果找到 'H' 并且后面有数字,返回匹配到的数字
546
+ return int(match.group(1))
547
+ else:
548
+ # 检查字符串中是否有 'H'(不论后面是否有数字)
549
+ if 'N' in s: # 使用 in 运算符检查字符是否存在[1,2](@ref)
550
+ return 1
551
+ else:
552
+ return 0
553
+
554
+ def parse_ion_pattern(ion_pattern):
555
+ """
556
+ 解析离子模式字符串,提取各个组成部分
557
+ 格式示例: [2M-2MeOH+6H]3+
558
+ """
559
+
560
+ # 主正则表达式匹配整个模式
561
+ pattern = r'\[(.*?)\]([-+]?\d*[+-]?)'
562
+ main_match = re.match(pattern, ion_pattern)
563
+
564
+ if not main_match:
565
+ return None
566
+
567
+ inner_content = main_match.group(1) # 方括号内的内容: 2M-2MeOH+6H
568
+ charge_str = main_match.group(2) # 电荷部分: 3+
569
+
570
+ # 解析电荷
571
+ charge_match = re.match(r'([+-]?\d*)([+-])?$', charge_str)
572
+ if charge_match:
573
+ charge_num = charge_match.group(1) or '1'
574
+ charge_sign = charge_match.group(2) or '+'
575
+ else:
576
+ charge_num = '1'
577
+ charge_sign = '+'
578
+
579
+ # 解析M前的数字
580
+ m_pattern = r'^(\d+)?M'
581
+ m_match = re.search(m_pattern, inner_content)
582
+ m_coefficient = m_match.group(1) if m_match and m_match.group(1) else '1'
583
+
584
+ # 解析所有基团
585
+ groups = []
586
+ # 正则表达式匹配各个基团: 可选数字 + 基团名称,前面可能有+或-号
587
+ group_pattern = r'([-+]?)(\d*)?([A-Za-z0-9]+)'
588
+
589
+ # 从第二个字符开始匹配,跳过第一个基团M
590
+ remaining_content = inner_content[len(m_match.group(0)):] if m_match else inner_content
591
+
592
+ for match in re.finditer(group_pattern, remaining_content):
593
+ if match.group(3): # 确保有基团名称
594
+ sign = match.group(1) or '+' # 如果没有符号,默认为+
595
+ coefficient = match.group(2) or '1' # 如果没有数字,默认为1
596
+ group_name = match.group(3)
597
+
598
+ groups.append({
599
+ 'group': group_name,
600
+ 'coefficient': coefficient,
601
+ 'sign': sign
602
+ })
603
+
604
+ return {
605
+ 'M_coefficient': m_coefficient,
606
+ 'groups': groups,
607
+ 'charge': {
608
+ 'number': charge_num,
609
+ 'sign': charge_sign
610
+ }
611
+ }
612
+
613
+ def get_mass(ion_pattern, mz):
614
+ result = parse_ion_pattern(ion_pattern)
615
+
616
+ add_number = 0
617
+ for group in result['groups']:
618
+ if group['group'] != 'M':
619
+ add_number = add_number - float(group['sign'] + str(
620
+ float(calculate_molecular_mass(group['group'])['mass']) * float(group['coefficient'])))
621
+ # print(add_number)
622
+ if result['charge']['number'] == '+' or result['charge']['number'] == '-':
623
+ result['charge']['number'] = 1
624
+
625
+ mz = (float(result['charge']['number']) * mz + add_number) / float(result['M_coefficient'])
626
+ # print(mz)
627
+ return mz
msformulator/cli.py ADDED
@@ -0,0 +1,43 @@
1
+ """cli.py - 命令行入口。
2
+
3
+ 安装本包后可通过 ``ms2-formula`` 命令调用,等价于
4
+ ``python -m msformulator.cli <pr_mz> <mz2> <type> <peak_type>``。
5
+ """
6
+ import argparse
7
+ import ast
8
+ import sys
9
+
10
+ from .main import main as _predict
11
+ from .download import ensure_weights
12
+
13
+
14
+ def main(argv=None):
15
+ parser = argparse.ArgumentParser(
16
+ prog="ms2-formula",
17
+ description="基于扩散模型的质谱(MS2)分子式预测。",
18
+ )
19
+ parser.add_argument("pr_mz", type=float, help="母离子精确质量 (float),如 439.326")
20
+ parser.add_argument(
21
+ "mz2",
22
+ type=str,
23
+ help='二级碎片峰列表,如 "[]" 或 "[[100.1, 200.0], [150.2, 80.0]]"',
24
+ )
25
+ parser.add_argument("type", type=str, help='母离子加合类型,如 "[M+H]+"')
26
+ parser.add_argument("peak_type", type=str, help='碎片峰加合类型,如 "[M+H]+"')
27
+ args = parser.parse_args(argv)
28
+
29
+ # 提前触发权重下载,给出清晰进度
30
+ ensure_weights()
31
+
32
+ result = _predict(
33
+ args.pr_mz,
34
+ ast.literal_eval(args.mz2),
35
+ args.type,
36
+ args.peak_type,
37
+ )
38
+ print(result)
39
+ return 0
40
+
41
+
42
+ if __name__ == "__main__":
43
+ sys.exit(main())
msformulator/config.py ADDED
@@ -0,0 +1,20 @@
1
+ """config.py - 包内资源路径配置。
2
+
3
+ 集中管理数据文件与模型权重的路径,避免在各模块中写死相对路径。
4
+ """
5
+ from pathlib import Path
6
+
7
+ # 本包目录,例如 .../msformulator
8
+ PACKAGE_DIR = Path(__file__).resolve().parent
9
+
10
+ # 数据文件目录(embedding_parameters.pth、mass_zhushi.npy),随包分发
11
+ DATA_DIR = PACKAGE_DIR / "data"
12
+
13
+ # 模型权重目录(两个 .pt 文件)不随包分发,运行时通过 download 模块解析
14
+ # (优先 MS2_WEIGHTS_DIR,否则缓存目录,缺失则自动下载)。
15
+ from .download import ensure_weights, weight_path, WEIGHT_FILES # noqa: E402
16
+
17
+
18
+ def get_weights_dir():
19
+ """返回权重所在目录(必要时触发下载)。"""
20
+ return ensure_weights()
Binary file