CoreMS 4.0.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.
Files changed (171) hide show
  1. corems/__init__.py +63 -0
  2. corems/chroma_peak/__init__.py +0 -0
  3. corems/chroma_peak/calc/ChromaPeakCalc.py +480 -0
  4. corems/chroma_peak/calc/__init__.py +0 -0
  5. corems/chroma_peak/calc/subset.py +196 -0
  6. corems/chroma_peak/factory/__init__.py +0 -0
  7. corems/chroma_peak/factory/chroma_peak_classes.py +1178 -0
  8. corems/encapsulation/__init__.py +0 -0
  9. corems/encapsulation/constant.py +1283 -0
  10. corems/encapsulation/factory/__init__.py +0 -0
  11. corems/encapsulation/factory/parameters.py +392 -0
  12. corems/encapsulation/factory/processingSetting.py +1244 -0
  13. corems/encapsulation/input/__init__.py +0 -0
  14. corems/encapsulation/input/parameter_from_json.py +579 -0
  15. corems/encapsulation/output/__init__.py +0 -0
  16. corems/encapsulation/output/parameter_to_dict.py +142 -0
  17. corems/encapsulation/output/parameter_to_json.py +332 -0
  18. corems/mass_spectra/__init__.py +0 -0
  19. corems/mass_spectra/calc/GC_Calc.py +84 -0
  20. corems/mass_spectra/calc/GC_Deconvolution.py +558 -0
  21. corems/mass_spectra/calc/GC_RI_Calibration.py +44 -0
  22. corems/mass_spectra/calc/MZSearch.py +249 -0
  23. corems/mass_spectra/calc/SignalProcessing.py +656 -0
  24. corems/mass_spectra/calc/__init__.py +0 -0
  25. corems/mass_spectra/calc/lc_calc.py +5790 -0
  26. corems/mass_spectra/calc/lc_calc_operations.py +1127 -0
  27. corems/mass_spectra/factory/GC_Class.py +791 -0
  28. corems/mass_spectra/factory/__init__.py +0 -0
  29. corems/mass_spectra/factory/chromat_data.py +51 -0
  30. corems/mass_spectra/factory/lc_class.py +2721 -0
  31. corems/mass_spectra/input/__init__.py +0 -0
  32. corems/mass_spectra/input/andiNetCDF.py +200 -0
  33. corems/mass_spectra/input/boosterHDF5.py +216 -0
  34. corems/mass_spectra/input/brukerSolarix.py +153 -0
  35. corems/mass_spectra/input/brukerSolarix_utils.py +73 -0
  36. corems/mass_spectra/input/corems_hdf5.py +1710 -0
  37. corems/mass_spectra/input/massList.py +133 -0
  38. corems/mass_spectra/input/mzml.py +668 -0
  39. corems/mass_spectra/input/parserbase.py +239 -0
  40. corems/mass_spectra/input/rawFileReader.py +1839 -0
  41. corems/mass_spectra/output/__init__.py +0 -0
  42. corems/mass_spectra/output/export.py +2800 -0
  43. corems/mass_spectrum/__init__.py +0 -0
  44. corems/mass_spectrum/calc/AutoRecalibration.py +237 -0
  45. corems/mass_spectrum/calc/Calibration.py +602 -0
  46. corems/mass_spectrum/calc/CalibrationCalc.py +253 -0
  47. corems/mass_spectrum/calc/KendrickGroup.py +239 -0
  48. corems/mass_spectrum/calc/MassErrorPrediction.py +436 -0
  49. corems/mass_spectrum/calc/MassSpectrumCalc.py +303 -0
  50. corems/mass_spectrum/calc/MeanResolvingPowerFilter.py +212 -0
  51. corems/mass_spectrum/calc/NoiseCalc.py +371 -0
  52. corems/mass_spectrum/calc/NoiseCalc_Bayes.py +93 -0
  53. corems/mass_spectrum/calc/PeakPicking.py +994 -0
  54. corems/mass_spectrum/calc/__init__.py +0 -0
  55. corems/mass_spectrum/factory/MassSpectrumClasses.py +1753 -0
  56. corems/mass_spectrum/factory/__init__.py +0 -0
  57. corems/mass_spectrum/input/__init__.py +0 -0
  58. corems/mass_spectrum/input/baseClass.py +531 -0
  59. corems/mass_spectrum/input/boosterHDF5.py +161 -0
  60. corems/mass_spectrum/input/coremsHDF5.py +475 -0
  61. corems/mass_spectrum/input/massList.py +402 -0
  62. corems/mass_spectrum/input/numpyArray.py +133 -0
  63. corems/mass_spectrum/output/__init__.py +0 -0
  64. corems/mass_spectrum/output/export.py +841 -0
  65. corems/molecular_formula/__init__.py +0 -0
  66. corems/molecular_formula/calc/MolecularFormulaCalc.py +885 -0
  67. corems/molecular_formula/calc/__init__.py +0 -0
  68. corems/molecular_formula/factory/MolecularFormulaFactory.py +895 -0
  69. corems/molecular_formula/factory/__init__.py +0 -0
  70. corems/molecular_formula/input/__init__.py +0 -0
  71. corems/molecular_formula/input/masslist_ref.py +355 -0
  72. corems/molecular_id/__init__.py +0 -0
  73. corems/molecular_id/calc/ClusterFilter.py +251 -0
  74. corems/molecular_id/calc/MolecularFilter.py +122 -0
  75. corems/molecular_id/calc/SpectralSimilarity.py +632 -0
  76. corems/molecular_id/calc/__init__.py +0 -0
  77. corems/molecular_id/calc/math_distance.py +1637 -0
  78. corems/molecular_id/data/FAMES_REF.msp +980 -0
  79. corems/molecular_id/data/PNNLMetV20191015.msp +157267 -0
  80. corems/molecular_id/factory/EI_SQL.py +650 -0
  81. corems/molecular_id/factory/MolecularLookupTable.py +914 -0
  82. corems/molecular_id/factory/__init__.py +0 -0
  83. corems/molecular_id/factory/classification.py +884 -0
  84. corems/molecular_id/factory/lipid_molecular_metadata.py +50 -0
  85. corems/molecular_id/factory/molecularSQL.py +827 -0
  86. corems/molecular_id/factory/spectrum_search_results.py +119 -0
  87. corems/molecular_id/input/__init__.py +0 -0
  88. corems/molecular_id/input/nistMSI.py +148 -0
  89. corems/molecular_id/search/__init__.py +0 -0
  90. corems/molecular_id/search/compoundSearch.py +214 -0
  91. corems/molecular_id/search/database_interfaces.py +1527 -0
  92. corems/molecular_id/search/findOxygenPeaks.py +330 -0
  93. corems/molecular_id/search/lcms_spectral_search.py +348 -0
  94. corems/molecular_id/search/molecularFormulaSearch.py +1117 -0
  95. corems/molecular_id/search/priorityAssignment.py +723 -0
  96. corems/ms_peak/__init__.py +0 -0
  97. corems/ms_peak/calc/MSPeakCalc.py +1010 -0
  98. corems/ms_peak/calc/__init__.py +0 -0
  99. corems/ms_peak/factory/MSPeakClasses.py +542 -0
  100. corems/ms_peak/factory/__init__.py +0 -0
  101. corems/transient/__init__.py +0 -0
  102. corems/transient/calc/TransientCalc.py +362 -0
  103. corems/transient/calc/__init__.py +0 -0
  104. corems/transient/factory/TransientClasses.py +457 -0
  105. corems/transient/factory/__init__.py +0 -0
  106. corems/transient/input/__init__.py +0 -0
  107. corems/transient/input/brukerSolarix.py +461 -0
  108. corems/transient/input/midasDatFile.py +172 -0
  109. corems-4.0.0.dist-info/METADATA +475 -0
  110. corems-4.0.0.dist-info/RECORD +171 -0
  111. corems-4.0.0.dist-info/WHEEL +5 -0
  112. corems-4.0.0.dist-info/licenses/LICENSE +22 -0
  113. corems-4.0.0.dist-info/top_level.txt +4 -0
  114. examples/archive/scripts/CoreMS_tutorial.py +94 -0
  115. examples/archive/scripts/DI HR-MS Halogens Bruker.py +196 -0
  116. examples/archive/scripts/DI HR-MS MassList.py +385 -0
  117. examples/archive/scripts/GC-MS MetabRef.py +213 -0
  118. examples/archive/scripts/GC-MS NetCDF.py +217 -0
  119. examples/archive/scripts/HR-MS Thermo Raw 21T.py +136 -0
  120. examples/archive/scripts/LC-ICPMS_metal_peaks.py +297 -0
  121. examples/archive/scripts/LCMS-Thermo.py +460 -0
  122. examples/archive/scripts/LCMS_isotopes.py +283 -0
  123. examples/archive/scripts/MSParams_example.py +21 -0
  124. examples/archive/scripts/Molecular Formula Data Aggreation.py +84 -0
  125. examples/archive/scripts/Single Mz Search.py +69 -0
  126. examples/test_notebooks.py +145 -0
  127. ext_lib/ChemstationMSFileReader.dll +0 -0
  128. ext_lib/ChemstationMSFileReader.xml +126 -0
  129. ext_lib/RawFileReaderLicense.doc +0 -0
  130. ext_lib/ThermoFisher.CommonCore.BackgroundSubtraction.dll +0 -0
  131. ext_lib/ThermoFisher.CommonCore.BackgroundSubtraction.xml +2307 -0
  132. ext_lib/ThermoFisher.CommonCore.Data.dll +0 -0
  133. ext_lib/ThermoFisher.CommonCore.Data.xml +28974 -0
  134. ext_lib/ThermoFisher.CommonCore.MassPrecisionEstimator.dll +0 -0
  135. ext_lib/ThermoFisher.CommonCore.MassPrecisionEstimator.xml +241 -0
  136. ext_lib/ThermoFisher.CommonCore.RawFileReader.dll +0 -0
  137. ext_lib/ThermoFisher.CommonCore.RawFileReader.xml +31174 -0
  138. ext_lib/__init__.py +0 -0
  139. ext_lib/dotnet/OpenMcdf.Extensions.dll +0 -0
  140. ext_lib/dotnet/OpenMcdf.dll +0 -0
  141. ext_lib/dotnet/OpenMcdf.xml +1154 -0
  142. ext_lib/dotnet/System.IO.FileSystem.AccessControl.dll +0 -0
  143. ext_lib/dotnet/System.IO.FileSystem.AccessControl.xml +506 -0
  144. ext_lib/dotnet/System.Security.AccessControl.dll +0 -0
  145. ext_lib/dotnet/System.Security.AccessControl.xml +2043 -0
  146. ext_lib/dotnet/System.Security.Principal.Windows.dll +0 -0
  147. ext_lib/dotnet/System.Security.Principal.Windows.xml +1011 -0
  148. ext_lib/dotnet/ThermoFisher.CommonCore.BackgroundSubtraction.dll +0 -0
  149. ext_lib/dotnet/ThermoFisher.CommonCore.BackgroundSubtraction.xml +2307 -0
  150. ext_lib/dotnet/ThermoFisher.CommonCore.Data.dll +0 -0
  151. ext_lib/dotnet/ThermoFisher.CommonCore.Data.xml +29148 -0
  152. ext_lib/dotnet/ThermoFisher.CommonCore.MassPrecisionEstimator.dll +0 -0
  153. ext_lib/dotnet/ThermoFisher.CommonCore.MassPrecisionEstimator.xml +241 -0
  154. ext_lib/dotnet/ThermoFisher.CommonCore.RawFileReader.dll +0 -0
  155. ext_lib/dotnet/ThermoFisher.CommonCore.RawFileReader.xml +31492 -0
  156. ext_lib/version +18 -0
  157. support_code/atom_parsers/AtomsDescription.py +359 -0
  158. support_code/atom_parsers/CreateAtomsDescription.py +56 -0
  159. support_code/nmdc/filefinder.py +90 -0
  160. support_code/nmdc/lipidomics/lipidomics_workflow.py +748 -0
  161. support_code/nmdc/lipidomics/manifest_examples.py +49 -0
  162. support_code/nmdc/metabolomics/gcms_workflow.py +158 -0
  163. support_code/nmdc/metabolomics/lcms_metabolomics_targeted_search.py +59 -0
  164. support_code/nmdc/metabolomics/lcms_metabolomics_workflow.py +248 -0
  165. support_code/nmdc/metabolomics/metabolomics_collection.py +628 -0
  166. support_code/nmdc/metadata/dms_api.py +42 -0
  167. support_code/nmdc/nom/archived_scripts/nmdc_metadata_gen.py +288 -0
  168. support_code/nmdc/nom/archived_scripts/nom_grow_workflow.py +209 -0
  169. support_code/nmdc/nom/nom_workflow.py +312 -0
  170. support_code/windows_only_importers/BrukerCompassXtract.py +180 -0
  171. support_code/windows_only_importers/ThermoMSFileReader.py +405 -0
corems/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ __author__ = "Yuri E. Corilo"
2
+ __version__ = "4.0.0"
3
+ import time
4
+ import os
5
+ import sys
6
+ import hashlib
7
+
8
+ # Get the path to the README file
9
+ readme_path = os.path.join(os.path.dirname(__file__), "..", "README.md")
10
+
11
+ # Read the contents of the README file if it exists
12
+ if os.path.exists(readme_path):
13
+ try:
14
+ with open(readme_path, "r", encoding="utf-8") as readme_file:
15
+ __doc__ = readme_file.read()
16
+ except Exception as e:
17
+ __doc__ = "CoreMS: A comprehensive mass spectrometry framework for software development and data analysis of small molecules analysis."
18
+ print(f"Warning: Could not read README.md file. Error: {e}")
19
+ else:
20
+ __doc__ = "CoreMS: A comprehensive mass spectrometry framework for software development and data analysis of small molecules analysis."
21
+
22
+
23
+
24
+ def timeit(print_time=True):
25
+ def decorator(method):
26
+ def timed(*args, **kw):
27
+ # Extract print_time from kwargs if provided
28
+ local_print_time = kw.pop('print_time', print_time)
29
+ ts = time.time()
30
+ result = method(*args, **kw)
31
+ te = time.time()
32
+ if "log_time" in kw:
33
+ name = kw.get("log_name", method.__name__.upper())
34
+ kw["log_time"][name] = int((te - ts) * 1000)
35
+ elif local_print_time:
36
+ print("%r %2.2f ms" % (method.__name__, (te - ts) * 1000))
37
+ return result
38
+ return timed
39
+ return decorator
40
+
41
+
42
+ class SuppressPrints:
43
+ def __enter__(self):
44
+ self._original_stdout = sys.stdout
45
+ sys.stdout = open(os.devnull, "w")
46
+
47
+ def __exit__(self, exc_type, exc_val, exc_tb):
48
+ sys.stdout.close()
49
+ sys.stdout = self._original_stdout
50
+
51
+
52
+ def chunks(lst, n):
53
+ """Yield successive n-sized chunks from lst."""
54
+ for i in range(0, len(lst), n):
55
+ yield lst[i : i + n]
56
+
57
+
58
+ def corems_md5(fname):
59
+ bytes_io = fname.open("rb").read()
60
+
61
+ md5_returned = hashlib.sha256(bytes_io).hexdigest()
62
+
63
+ return "{}:{}".format("sha256", md5_returned)
File without changes
@@ -0,0 +1,480 @@
1
+ import numpy as np
2
+ from bisect import bisect_left
3
+ from scipy.optimize import curve_fit
4
+
5
+ try:
6
+ np.trapezoid
7
+ except AttributeError: # numpy < 2.0
8
+ np.trapezoid = np.trapz
9
+
10
+
11
+ __author__ = "Yuri E. Corilo"
12
+ __date__ = "March 11, 2020"
13
+
14
+
15
+ class GCPeakCalculation(object):
16
+ """
17
+ Class for performing peak calculations in GC chromatography.
18
+
19
+ Methods
20
+ -------
21
+ * `calc_area(self, tic: List[float], dx: float) -> None`: Calculate the area under the curve of the chromatogram.
22
+ * `linear_ri(self, right_ri: float, left_ri: float, left_rt: float, right_rt: float) -> float`: Calculate the retention index using linear interpolation.
23
+ * `calc_ri(self, rt_ri_pairs: List[Tuple[float, float]]) -> int`: Calculate the retention index based on the given retention time - retention index pairs.
24
+ """
25
+
26
+ def calc_area(self, tic: list[float], dx: float) -> None:
27
+ """
28
+ Calculate the area under the curve of the chromatogram.
29
+
30
+ Parameters
31
+ ----------
32
+ tic : List[float]
33
+ The total ion current (TIC) values.
34
+ dx : float
35
+ The spacing between data points.
36
+ """
37
+ yy = tic[self.start_scan : self.final_scan]
38
+ self._area = np.trapezoid(yy, dx=dx)
39
+
40
+ def linear_ri(
41
+ self, right_ri: float, left_ri: float, left_rt: float, right_rt: float
42
+ ) -> float:
43
+ """
44
+ Calculate the retention index using linear interpolation.
45
+
46
+ Parameters
47
+ ----------
48
+ right_ri : float
49
+ The retention index at the right reference point.
50
+ left_ri : float
51
+ The retention index at the left reference point.
52
+ left_rt : float
53
+ The retention time at the left reference point.
54
+ right_rt : float
55
+ The retention time at the right reference point.
56
+
57
+ Returns
58
+ -------
59
+ float
60
+ The calculated retention index.
61
+ """
62
+ return left_ri + (
63
+ (right_ri - left_ri)
64
+ * (self.retention_time - left_rt)
65
+ / (right_rt - left_rt)
66
+ )
67
+
68
+ def calc_ri(self, rt_ri_pairs: list[tuple[float, float]]) -> None:
69
+ """
70
+ Calculate the retention index based on the given retention time - retention index pairs.
71
+
72
+ Parameters
73
+ ----------
74
+ rt_ri_pairs : List[Tuple[float, float]]
75
+ The list of retention time - retention index pairs.
76
+
77
+ """
78
+ current_rt = self.retention_time
79
+
80
+ rts = [rt_ri[0] for rt_ri in rt_ri_pairs]
81
+ index = bisect_left(rts, current_rt)
82
+
83
+ if index >= len(rt_ri_pairs):
84
+ index -= 1
85
+
86
+ current_ref = rt_ri_pairs[index]
87
+
88
+ if current_rt == current_ref[0]:
89
+ self._ri = current_ref[1]
90
+
91
+ else:
92
+ if index == 0:
93
+ index += 1
94
+
95
+ left_rt = rt_ri_pairs[index - 1][0]
96
+ left_ri = rt_ri_pairs[index - 1][1]
97
+
98
+ right_rt = rt_ri_pairs[index][0]
99
+ right_ri = rt_ri_pairs[index][1]
100
+
101
+ self._ri = self.linear_ri(right_ri, left_ri, left_rt, right_rt)
102
+
103
+
104
+ class LCMSMassFeatureCalculation:
105
+ """Class for performing peak calculations in LC-MS mass spectrometry.
106
+
107
+ This class is intended to be used as a mixin class for the LCMSMassFeature class.
108
+ """
109
+
110
+ def calc_dispersity_index(self):
111
+ """
112
+ Calculate the dispersity index of the mass feature.
113
+
114
+ This function calculates the dispersity index of the mass feature and
115
+ stores the result in the `_dispersity_index` attribute. The dispersity index is calculated as the standard
116
+ deviation of the retention times that account for 50% of the cummulative intensity, starting from the most
117
+ intense point, as described in [1]. Note that this calculation is done within the integration bounds with
118
+ a pad according to the window factor, where the window factor is parameterized and encapsulated in the
119
+ parent LCMS object (or, if not available, defaults to 2.0 minutes before and after the apex
120
+
121
+ Returns
122
+ -------
123
+ None, stores the result in the `_dispersity_index` attribute of the class and the `_normalized_dispersity_index` attribute,
124
+ which is the dispersity index normalized to the total time window used for the calculation (unitless, fraction of total window).
125
+
126
+ Raises
127
+ ------
128
+ ValueError
129
+ If the EIC data are not available.
130
+
131
+ References
132
+ ----------
133
+ 1) Boiteau, Rene M., et al. "Relating Molecular Properties to the Persistence of Marine Dissolved
134
+ Organic Matter with Liquid Chromatography–Ultrahigh-Resolution Mass Spectrometry."
135
+ Environmental Science & Technology 58.7 (2024): 3267-3277.
136
+ """
137
+ # Check if LCMSMassFeature has a parent LCMS object with a window factor
138
+ if hasattr(self, "mass_spectrum_obj"):
139
+ window_min = self.mass_spectrum_obj.parameters.lc_ms.dispersity_index_window
140
+ else:
141
+ window_min = 3.0 # minutes
142
+
143
+ # Check if the EIC data is available
144
+ if self.eic_list is None:
145
+ raise ValueError(
146
+ "EIC data are not available. Please add the EIC data first."
147
+ )
148
+
149
+ # Define start and end of the window around the apex
150
+ apex_rt = self.retention_time
151
+ full_time = self._eic_data.time
152
+ full_eic = self._eic_data.eic
153
+ left_start = apex_rt - window_min
154
+ right_end = apex_rt + window_min
155
+
156
+ # Extract the EIC data within the defined window
157
+ time_mask = (full_time >= left_start) & (full_time <= right_end)
158
+ eic_subset = full_eic[time_mask]
159
+ time_subset = full_time[time_mask]
160
+
161
+ # Sort the EIC data and RT data by descending intensity
162
+ sorted_eic = eic_subset[eic_subset.argsort()[::-1]]
163
+ sorted_rt = time_subset[eic_subset.argsort()[::-1]]
164
+
165
+ # Calculate the dispersity index
166
+ cum_sum = np.cumsum(sorted_eic) / np.sum(sorted_eic)
167
+ rt_summ = sorted_rt[np.where(cum_sum < 0.5)]
168
+ if len(rt_summ) > 1:
169
+ d = np.std(rt_summ)
170
+ self._dispersity_index = d # minutes
171
+ self._normalized_dispersity_index = d / (
172
+ time_subset[-1] - time_subset[0]
173
+ ) # unitless (fraction of total window used)
174
+ elif len(rt_summ) == 1:
175
+ self._dispersity_index = 0
176
+ self._normalized_dispersity_index = 0
177
+
178
+ def calc_fraction_height_width(self, fraction: float):
179
+ """
180
+ Calculate the height width of the mass feature at a specfic fraction of the maximum intensity.
181
+
182
+ This function returns a tuple with the minimum and maximum half-height width based on scan resolution.
183
+
184
+ Parameters
185
+ ----------
186
+ fraction : float
187
+ The fraction of the maximum intensity to calculate the height width.
188
+ For example, 0.5 will calculate the half-height width.
189
+
190
+ Returns
191
+ -------
192
+ Tuple[float, float, bool]
193
+ The minimum and maximum half-height width based on scan resolution (in minutes), and a boolean indicating if the width was estimated.
194
+ """
195
+
196
+ # Pull out the EIC data
197
+ eic = self._eic_data.eic_smoothed
198
+
199
+ # Find the indices of the maximum intensity on either side
200
+ max_index = np.where(self._eic_data.scans == self.apex_scan)[0][0]
201
+ left_index = max_index
202
+ right_index = max_index
203
+ while eic[left_index] > eic[max_index] * fraction and left_index > 0:
204
+ left_index -= 1
205
+ while (
206
+ eic[right_index] > eic[max_index] * fraction and right_index < len(eic) - 1
207
+ ):
208
+ right_index += 1
209
+
210
+ # Get the retention times of the indexes just below the half height
211
+ left_rt = self._eic_data.time[left_index]
212
+ right_rt = self._eic_data.time[right_index]
213
+
214
+ # If left_rt and right_rt are outside the bounds of the integration, set them to the bounds and set estimated to True
215
+ estimated = False
216
+ if left_rt < self.eic_rt_list[0]:
217
+ left_rt = self.eic_rt_list[0]
218
+ left_index = np.where(self._eic_data.scans == self._eic_data.apexes[0][0])[
219
+ 0
220
+ ][0]
221
+ estimated = True
222
+ if right_rt > self.eic_rt_list[-1]:
223
+ right_rt = self.eic_rt_list[-1]
224
+ right_index = np.where(
225
+ self._eic_data.scans == self._eic_data.apexes[0][-1]
226
+ )[0][0]
227
+ estimated = True
228
+ half_height_width_max = right_rt - left_rt
229
+
230
+ # Get the retention times of the indexes just above the half height
231
+ left_rt = self._eic_data.time[left_index + 1]
232
+ right_rt = self._eic_data.time[right_index - 1]
233
+ half_height_width_min = right_rt - left_rt
234
+
235
+ return half_height_width_min, half_height_width_max, estimated
236
+
237
+ def calc_half_height_width(self, accept_estimated: bool = False):
238
+ """
239
+ Calculate the half-height width of the mass feature.
240
+
241
+ This function calculates the half-height width of the mass feature and
242
+ stores the result in the `_half_height_width` attribute
243
+
244
+ Returns
245
+ -------
246
+ None, stores the result in the `_half_height_width` attribute of the class.
247
+ """
248
+ min_, max_, estimated = self.calc_fraction_height_width(0.5)
249
+ if not estimated or accept_estimated:
250
+ self._half_height_width = np.array([min_, max_])
251
+
252
+ def calc_tailing_factor(self, accept_estimated: bool = False):
253
+ """
254
+ Calculate the peak asymmetry of the mass feature.
255
+
256
+ This function calculates the peak asymmetry of the mass feature and
257
+ stores the result in the `_tailing_factor` attribute.
258
+ Calculations completed at 5% of the peak height in accordance with the USP tailing factor calculation.
259
+
260
+ Returns
261
+ -------
262
+ None, stores the result in the `_tailing_factor` attribute of the class.
263
+
264
+ References
265
+ ----------
266
+ 1) JIS K0124:2011 General rules for high performance liquid chromatography
267
+ 2) JIS K0214:2013 Technical terms for analytical chemistry
268
+ """
269
+ # First calculate the width of the peak at 5% of the peak height
270
+ width_min, width_max, estimated = self.calc_fraction_height_width(0.05)
271
+
272
+ if not estimated or accept_estimated:
273
+ # Next calculate the width of the peak at 95% of the peak height
274
+ eic = self._eic_data.eic_smoothed
275
+ max_index = np.where(self._eic_data.scans == self.apex_scan)[0][0]
276
+ left_index = max_index
277
+ while eic[left_index] > eic[max_index] * 0.05 and left_index > 0:
278
+ left_index -= 1
279
+
280
+ left_half_time_min = (
281
+ self._eic_data.time[max_index] - self._eic_data.time[left_index]
282
+ )
283
+ left_half_time_max = (
284
+ self._eic_data.time[max_index] - self._eic_data.time[left_index + 1]
285
+ )
286
+
287
+ tailing_factor = np.mean([width_min, width_max]) / (
288
+ 2 * np.mean([left_half_time_min, left_half_time_max])
289
+ )
290
+
291
+ self._tailing_factor = tailing_factor
292
+
293
+ def calc_gaussian_similarity(self):
294
+ """
295
+ Calculate the Gaussian similarity score of the mass feature.
296
+
297
+ This function fits a Gaussian curve to the EIC data and evaluates
298
+ the goodness of fit using R-squared. Note that this only uses data within
299
+ the set integration bounds of the mass feature. A score close to 1 indicates
300
+ the peak closely resembles an ideal Gaussian shape.
301
+
302
+ Returns
303
+ -------
304
+ None, stores the result in the `_gaussian_similarity` attribute of the class.
305
+
306
+ Raises
307
+ ------
308
+ ValueError
309
+ If the EIC data are not available.
310
+ """
311
+ # Check if the EIC data is available
312
+ if self.eic_list is None:
313
+ raise ValueError(
314
+ "EIC data are not available. Please add the EIC data first."
315
+ )
316
+
317
+ # Get EIC data within integration bounds
318
+ time_data = np.array(self.eic_rt_list)
319
+ intensity_data = np.array(self.eic_list)
320
+
321
+ if len(time_data) < 4: # Need minimum points for meaningful fit
322
+ self._gaussian_similarity = np.nan
323
+ return
324
+
325
+ # Check for valid intensity data
326
+ max_intensity = np.max(intensity_data)
327
+ if max_intensity == 0:
328
+ self._gaussian_similarity = np.nan
329
+ return
330
+
331
+ try:
332
+ # Define Gaussian function
333
+ def gaussian(x, amplitude, mean, stddev, baseline):
334
+ return (
335
+ amplitude * np.exp(-((x - mean) ** 2) / (2 * stddev**2)) + baseline
336
+ )
337
+
338
+ # Initial parameter estimates
339
+ amplitude_init = max_intensity
340
+ mean_init = time_data[np.argmax(intensity_data)]
341
+ stddev_init = (time_data[-1] - time_data[0]) / 6 # Rough estimate
342
+ baseline_init = np.min(intensity_data)
343
+
344
+ # Fit Gaussian curve
345
+ popt, _ = curve_fit(
346
+ gaussian,
347
+ time_data,
348
+ intensity_data,
349
+ p0=[amplitude_init, mean_init, stddev_init, baseline_init],
350
+ maxfev=1000,
351
+ bounds=(
352
+ [0, time_data[0], 0, 0], # Lower bounds
353
+ [np.inf, time_data[-1], np.inf, max_intensity], # Upper bounds
354
+ ),
355
+ )
356
+
357
+ # Calculate fitted values
358
+ fitted_intensities = gaussian(time_data, *popt)
359
+
360
+ # Calculate R-squared (coefficient of determination)
361
+ ss_res = np.sum((intensity_data - fitted_intensities) ** 2)
362
+ ss_tot = np.sum((intensity_data - np.mean(intensity_data)) ** 2)
363
+
364
+ if ss_tot == 0:
365
+ self._gaussian_similarity = np.nan
366
+ else:
367
+ r_squared = 1 - (ss_res / ss_tot)
368
+ # R² should be between 0 and 1 for reasonable fits
369
+ # If negative, the model is worse than the mean - treat as non-computable
370
+ self._gaussian_similarity = r_squared if r_squared >= 0 else np.nan
371
+
372
+ except (RuntimeError, ValueError, TypeError):
373
+ # Fitting failed, assign NaN
374
+ self._gaussian_similarity = np.nan
375
+
376
+ def calc_noise_score(self):
377
+ """
378
+ Calculate the noise score of the mass feature separately for left and right sides.
379
+
380
+ This function estimates the signal-to-noise ratio by comparing the peak
381
+ intensity to the baseline noise level in surrounding regions. It calculates
382
+ separate scores for the left and right sides of the peak, which are stored as a tuple
383
+ in the `_noise_score` attribute. The noise estimation windows are encapsulated in the
384
+ parent LCMS object (or, if not available, defaults to twice the peak width on each side).
385
+
386
+
387
+ Returns
388
+ -------
389
+ None, stores the result in the `_noise_score` attribute as a tuple (left_score, right_score).
390
+
391
+ Raises
392
+ ------
393
+ ValueError
394
+ If the EIC data are not available.
395
+ """
396
+ # Check if the EIC data is available
397
+ if self.eic_list is None:
398
+ raise ValueError(
399
+ "EIC data are not available. Please add the EIC data first."
400
+ )
401
+
402
+ # Check if LCMSMassFeature has a parent LCMS object with a window factor
403
+ if hasattr(self, "mass_spectrum_obj"):
404
+ noise_window_factor = (
405
+ self.mass_spectrum_obj.parameters.lc_ms.noise_window_factor
406
+ )
407
+ else:
408
+ noise_window_factor = 2.0 # times the peak width
409
+
410
+ # Get full EIC data (not just integration bounds)
411
+ full_time = self._eic_data.time
412
+ full_eic = self._eic_data.eic
413
+
414
+ # Get peak information
415
+ apex_rt = self.retention_time
416
+ peak_intensity = np.max(self.eic_list)
417
+
418
+ # Retrieve width from integration bounds
419
+ peak_width = self.eic_rt_list[-1] - self.eic_rt_list[0]
420
+
421
+ # Define noise estimation windows
422
+ noise_window_size = peak_width * noise_window_factor # in minutes
423
+ left_noise_start = apex_rt - peak_width - noise_window_size
424
+ left_noise_end = apex_rt - peak_width
425
+ right_noise_start = apex_rt + peak_width
426
+ right_noise_end = apex_rt + peak_width + noise_window_size
427
+
428
+ # Extract noise regions
429
+ left_noise_mask = (full_time >= left_noise_start) & (
430
+ full_time <= left_noise_end
431
+ )
432
+ right_noise_mask = (full_time >= right_noise_start) & (
433
+ full_time <= right_noise_end
434
+ )
435
+
436
+ left_noise = full_eic[left_noise_mask]
437
+ right_noise = full_eic[right_noise_mask]
438
+
439
+ # Calculate left noise score
440
+ if len(left_noise) == 0:
441
+ left_score = np.nan
442
+ else:
443
+ left_baseline = np.median(left_noise)
444
+ left_noise_std = np.std(left_noise)
445
+
446
+ if left_noise_std == 0:
447
+ if peak_intensity > left_baseline:
448
+ left_score = 1.0
449
+ else:
450
+ left_score = np.nan
451
+ else:
452
+ left_signal = peak_intensity - left_baseline
453
+ if left_signal <= 0:
454
+ left_score = 0.0
455
+ else:
456
+ left_snr = left_signal / left_noise_std
457
+ left_score = min(1.0, left_snr / (left_snr + 10.0))
458
+
459
+ # Calculate right noise score
460
+ if len(right_noise) == 0:
461
+ right_score = np.nan
462
+ else:
463
+ right_baseline = np.median(right_noise)
464
+ right_noise_std = np.std(right_noise)
465
+
466
+ if right_noise_std == 0:
467
+ if peak_intensity > right_baseline:
468
+ right_score = 1.0
469
+ else:
470
+ right_score = np.nan
471
+ else:
472
+ right_signal = peak_intensity - right_baseline
473
+ if right_signal <= 0:
474
+ right_score = 0.0
475
+ else:
476
+ right_snr = right_signal / right_noise_std
477
+ right_score = min(1.0, right_snr / (right_snr + 10.0))
478
+
479
+ # Store as tuple
480
+ self._noise_score = (left_score, right_score)
File without changes