corems.mass_spectrum.input.baseClass
1__author__ = "Yuri E. Corilo" 2__date__ = "Nov 11, 2019" 3 4from copy import deepcopy 5from io import BytesIO 6from pathlib import Path 7 8import chardet 9from bs4 import BeautifulSoup 10from pandas import read_csv, read_excel, read_pickle 11from pandas.core.frame import DataFrame 12from s3path import S3Path 13 14from corems.encapsulation.constant import Labels 15from corems.encapsulation.factory.parameters import default_parameters 16from corems.encapsulation.factory.processingSetting import DataInputSetting 17from corems.encapsulation.input.parameter_from_json import ( 18 load_and_set_parameters_class, 19 load_and_set_parameters_ms, 20 load_and_set_toml_parameters_class, 21) 22 23 24class MassListBaseClass: 25 """The MassListBaseClass object reads mass list data types and returns the mass spectrum obj 26 27 Parameters 28 ---------- 29 file_location : Path or S3Path 30 Full data path. 31 isCentroid : bool, optional 32 Determines the mass spectrum data structure. If set to True, it assumes centroid mode. If set to False, it assumes profile mode and attempts to peak pick. Default is True. 33 analyzer : str, optional 34 The analyzer used for the mass spectrum. Default is 'Unknown'. 35 instrument_label : str, optional 36 The label of the instrument used for the mass spectrum. Default is 'Unknown'. 37 sample_name : str, optional 38 The name of the sample. Default is None. 39 header_lines : int, optional 40 The number of lines to skip in the file, including the column labels line. Default is 0. 41 isThermoProfile : bool, optional 42 Determines the number of expected columns in the file. If set to True, only m/z and intensity columns are expected. Signal-to-noise ratio (S/N) and resolving power (RP) will be calculated based on the data. Default is False. 43 headerless : bool, optional 44 If True, assumes that there are no headers present in the file (e.g., a .xy file from Bruker) and assumes two columns: m/z and intensity. Default is False. 45 46 Attributes 47 ---------- 48 parameters : DataInputSetting 49 The data input settings for the mass spectrum. 50 data_type : str 51 The type of data in the file. 52 delimiter : str 53 The delimiter used to read text-based files. 54 55 Methods 56 ------- 57 * set_parameter_from_toml(parameters_path). Sets the data input settings from a TOML file. 58 * set_parameter_from_json(parameters_path). Sets the data input settings from a JSON file. 59 * get_dataframe(). Reads the file and returns the data as a pandas DataFrame. 60 * load_settings(mass_spec_obj, output_parameters). Loads the settings for the mass spectrum. 61 * get_output_parameters(polarity, scan_index=0). Returns the output parameters for the mass spectrum. 62 * clean_data_frame(dataframe). Cleans the data frame by removing columns that are not in the expected columns set. 63 64 """ 65 66 def __init__( 67 self, 68 file_location: Path | S3Path, 69 isCentroid: bool = True, 70 analyzer: str = "Unknown", 71 instrument_label: str = "Unknown", 72 sample_name: str = None, 73 header_lines: int = 0, 74 isThermoProfile: bool = False, 75 headerless: bool = False, 76 ): 77 self.file_location = ( 78 Path(file_location) if isinstance(file_location, str) else file_location 79 ) 80 81 if not self.file_location.exists(): 82 raise FileExistsError("File does not exist: %s" % file_location) 83 84 # (newline="\n") 85 86 self.header_lines = header_lines 87 88 if isThermoProfile: 89 self._expected_columns = {Labels.mz, Labels.abundance} 90 91 else: 92 self._expected_columns = { 93 Labels.mz, 94 Labels.abundance, 95 Labels.s2n, 96 Labels.rp, 97 } 98 99 self._delimiter = None 100 101 self.isCentroid = isCentroid 102 103 self.isThermoProfile = isThermoProfile 104 105 self.headerless = headerless 106 107 self._data_type = None 108 109 self.analyzer = analyzer 110 111 self.instrument_label = instrument_label 112 113 self.sample_name = sample_name 114 115 self._parameters = deepcopy(DataInputSetting()) 116 117 @property 118 def parameters(self): 119 return self._parameters 120 121 @parameters.setter 122 def parameters(self, instance_DataInputSetting): 123 self._parameters = instance_DataInputSetting 124 125 def set_parameter_from_toml(self, parameters_path): 126 self._parameters = load_and_set_toml_parameters_class( 127 "DataInput", self.parameters, parameters_path=parameters_path 128 ) 129 130 def set_parameter_from_json(self, parameters_path): 131 self._parameters = load_and_set_parameters_class( 132 "DataInput", self.parameters, parameters_path=parameters_path 133 ) 134 135 @property 136 def data_type(self): 137 return self._data_type 138 139 @data_type.setter 140 def data_type(self, data_type): 141 self._data_type = data_type 142 143 @property 144 def delimiter(self): 145 return self._delimiter 146 147 @delimiter.setter 148 def delimiter(self, delimiter): 149 self._delimiter = delimiter 150 151 def encoding_detector(self, file_location) -> str: 152 """ 153 Detects the encoding of a file. 154 155 Parameters 156 -------- 157 file_location : str 158 The location of the file to be analyzed. 159 160 Returns 161 -------- 162 str 163 The detected encoding of the file. 164 """ 165 166 with file_location.open("rb") as rawdata: 167 result = chardet.detect(rawdata.read(10000)) 168 return result["encoding"] 169 170 def set_data_type(self): 171 """ 172 Set the data type and delimiter based on the file extension. 173 174 Raises 175 ------ 176 TypeError 177 If the data type could not be automatically recognized. 178 """ 179 if self.file_location.suffix == ".csv": 180 self.data_type = "txt" 181 self.delimiter = "," 182 elif self.file_location.suffix == ".txt": 183 self.data_type = "txt" 184 self.delimiter = "\t" 185 elif self.file_location.suffix == ".tsv": 186 self.data_type = "txt" 187 self.delimiter = "\t" 188 elif self.file_location.suffix == ".xlsx": 189 self.data_type = "excel" 190 elif self.file_location.suffix == ".ascii": 191 self.data_type = "txt" 192 self.delimiter = " " 193 elif self.file_location.suffix == ".pkl": 194 self.data_type = "dataframe" 195 elif self.file_location.suffix == ".pks": 196 self.data_type = "pks" 197 self.delimiter = " " 198 self.header_lines = 9 199 elif self.file_location.suffix == ".xml": 200 self.data_type = "xml" 201 # self.delimiter = None 202 # self.header_lines = None 203 elif self.file_location.suffix == ".xy": 204 self.data_type = "txt" 205 self.delimiter = " " 206 self.header_lines = None 207 else: 208 raise TypeError( 209 "Data type could not be automatically recognized for %s; please set data type and delimiter manually." 210 % self.file_location.name 211 ) 212 213 def get_dataframe(self) -> DataFrame: 214 """ 215 Get the data as a pandas DataFrame. 216 217 Returns 218 ------- 219 pandas.DataFrame 220 The data as a pandas DataFrame. 221 222 Raises 223 ------ 224 TypeError 225 If the data type is not supported. 226 """ 227 228 if not self.data_type or not self.delimiter: 229 self.set_data_type() 230 231 if isinstance(self.file_location, S3Path): 232 data = BytesIO(self.file_location.open("rb").read()) 233 else: 234 data = self.file_location 235 236 if self.data_type == "txt": 237 if self.headerless: 238 dataframe = read_csv( 239 data, 240 skiprows=self.header_lines, 241 delimiter=self.delimiter, 242 header=None, 243 names=["m/z", "I"], 244 encoding=self.encoding_detector(self.file_location), 245 engine="python", 246 ) 247 else: 248 dataframe = read_csv( 249 data, 250 skiprows=self.header_lines, 251 delimiter=self.delimiter, 252 encoding=self.encoding_detector(self.file_location), 253 engine="python", 254 ) 255 256 elif self.data_type == "pks": 257 # Predator .pks columns are positional: peak location (m/z), relative 258 # peak height (normalized 0-100), absolute abundance, resolving power, 259 # frequency, S/N. Use the absolute abundance as the intensity -- the 260 # relative peak height is per-spectrum normalized and not comparable 261 # across spectra, so it is named so header_translate drops it. 262 names = [ 263 "m/z", 264 "Relative Abundance", 265 "Abundance", 266 "Resolving Power", 267 "Frequency", 268 "S/N" 269 ] 270 271 272 clean_data = [] 273 with self.file_location.open() as maglabfile: 274 for i in maglabfile.readlines()[8:-1]: 275 clean_data.append(i.split()) 276 dataframe = DataFrame(clean_data, columns=names) 277 278 elif self.data_type == "dataframe": 279 dataframe = read_pickle(data) 280 281 elif self.data_type == "excel": 282 dataframe = read_excel(data) 283 284 elif self.data_type == "xml": 285 dataframe = self.read_xml_peaks(data) 286 287 else: 288 raise TypeError("Data type %s is not supported" % self.data_type) 289 290 return dataframe 291 292 def load_settings(self, mass_spec_obj, output_parameters): 293 """ 294 #TODO loading output parameters from json file is not functional 295 Load settings from a JSON file and apply them to the given mass_spec_obj. 296 297 Parameters 298 ---------- 299 mass_spec_obj : MassSpec 300 The mass spectrum object to apply the settings to. 301 302 """ 303 import json 304 import warnings 305 306 settings_file_path = self.file_location.with_suffix(".json") 307 308 if settings_file_path.exists(): 309 self._parameters = load_and_set_parameters_class( 310 "DataInput", self._parameters, parameters_path=settings_file_path 311 ) 312 313 load_and_set_parameters_ms( 314 mass_spec_obj, parameters_path=settings_file_path 315 ) 316 317 else: 318 warnings.warn( 319 "auto settings loading is enabled but could not locate the file: %s. Please load the settings manually" 320 % settings_file_path 321 ) 322 323 # TODO this will load the setting from SettingCoreMS.json 324 # coreMSHFD5 overrides this function to import the attrs stored in the h5 file 325 # loaded_settings = {} 326 # loaded_settings['MoleculaSearch'] = self.get_scan_group_attr_data(scan_index, time_index, 'MoleculaSearchSetting') 327 # loaded_settings['MassSpecPeak'] = self.get_scan_group_attr_data(scan_index, time_index, 'MassSpecPeakSetting') 328 329 # loaded_settings['MassSpectrum'] = self.get_scan_group_attr_data(scan_index, time_index, 'MassSpectrumSetting') 330 # loaded_settings['Transient'] = self.get_scan_group_attr_data(scan_index, time_index, 'TransientSetting') 331 332 def get_output_parameters(self, polarity: int, scan_index: int = 0) -> dict: 333 """ 334 Get the output parameters for the mass spectrum. 335 336 Parameters 337 ---------- 338 polarity : int 339 The polarity of the mass spectrum +1 or -1. 340 scan_index : int, optional 341 The index of the scan. Default is 0. 342 343 Returns 344 ------- 345 dict 346 A dictionary containing the output parameters. 347 348 """ 349 from copy import deepcopy 350 351 output_parameters = default_parameters(self.file_location) 352 353 if self.isCentroid: 354 output_parameters["label"] = Labels.corems_centroid 355 else: 356 output_parameters["label"] = Labels.bruker_profile 357 358 output_parameters["analyzer"] = self.analyzer 359 360 output_parameters["instrument_label"] = self.instrument_label 361 362 output_parameters["sample_name"] = self.sample_name 363 364 output_parameters["Aterm"] = None 365 366 output_parameters["Bterm"] = None 367 368 output_parameters["Cterm"] = None 369 370 output_parameters["polarity"] = polarity 371 372 # scan_number and rt will be need to lc ms==== 373 374 output_parameters["mobility_scan"] = 0 375 376 output_parameters["mobility_rt"] = 0 377 378 output_parameters["scan_number"] = scan_index 379 380 output_parameters["rt"] = 0 381 382 return output_parameters 383 384 def clean_data_frame(self, dataframe): 385 """ 386 Clean the input dataframe by removing columns that are not expected. 387 388 Parameters 389 ---------- 390 pandas.DataFrame 391 The input dataframe to be cleaned. 392 393 """ 394 395 for column_name in dataframe.columns: 396 expected_column_name = self.parameters.header_translate.get(column_name) 397 if expected_column_name not in self._expected_columns: 398 del dataframe[column_name] 399 400 def check_columns(self, header_labels: list[str]): 401 """ 402 Check if the given header labels match the expected columns. 403 404 Parameters 405 ---------- 406 header_labels : list 407 The header labels to be checked. 408 409 Raises 410 ------ 411 Exception 412 If any expected column is not found in the header labels. 413 """ 414 found_label = set() 415 416 for label in header_labels: 417 if not label in self._expected_columns: 418 user_column_name = self.parameters.header_translate.get(label) 419 if user_column_name in self._expected_columns: 420 found_label.add(user_column_name) 421 else: 422 found_label.add(label) 423 424 not_found = self._expected_columns - found_label 425 426 if len(not_found) > 0: 427 raise Exception( 428 "Please make sure to include the columns %s" % ", ".join(not_found) 429 ) 430 431 def read_xml_peaks(self, data: str) -> DataFrame: 432 """ 433 Read peaks from a Bruker .xml file and return a pandas DataFrame. 434 435 Parameters 436 ---------- 437 data : str 438 The path to the .xml file. 439 440 Returns 441 ------- 442 pandas.DataFrame 443 A DataFrame containing the peak data with columns: 'm/z', 'I', 'Resolving Power', 'Area', 'S/N', 'fwhm'. 444 """ 445 from numpy import nan 446 447 with open(data, "r") as file: 448 content = file.readlines() 449 content = "".join(content) 450 bs_content = BeautifulSoup(content, features="xml") 451 peaks_xml = bs_content.find_all("pk") 452 453 # initialise lists of the peak variables 454 areas = [] 455 fwhms = [] 456 intensities = [] 457 mzs = [] 458 res = [] 459 sn = [] 460 # iterate through the peaks appending to each list 461 for peak in peaks_xml: 462 areas.append( 463 float(peak.get("a", nan)) 464 ) # Use a default value if key 'a' is missing 465 fwhms.append( 466 float(peak.get("fwhm", nan)) 467 ) # Use a default value if key 'fwhm' is missing 468 intensities.append( 469 float(peak.get("i", nan)) 470 ) # Use a default value if key 'i' is missing 471 mzs.append( 472 float(peak.get("mz", nan)) 473 ) # Use a default value if key 'mz' is missing 474 res.append( 475 float(peak.get("res", nan)) 476 ) # Use a default value if key 'res' is missing 477 sn.append( 478 float(peak.get("sn", nan)) 479 ) # Use a default value if key 'sn' is missing 480 481 # Compile pandas dataframe of these values 482 names = ["m/z", "I", "Resolving Power", "Area", "S/N", "fwhm"] 483 df = DataFrame(columns=names, dtype=float) 484 df["m/z"] = mzs 485 df["I"] = intensities 486 df["Resolving Power"] = res 487 df["Area"] = areas 488 df["S/N"] = sn 489 df["fwhm"] = fwhms 490 return df 491 492 def get_xml_polarity(self): 493 """ 494 Get the polarity from an XML peaklist. 495 496 Returns 497 ------- 498 int 499 The polarity of the XML peaklist. Returns -1 for negative polarity, +1 for positive polarity. 500 501 Raises 502 ------ 503 Exception 504 If the data type is not XML peaklist in Bruker format or if the polarity is unhandled. 505 """ 506 507 # Check its an actual xml 508 if not self.data_type or not self.delimiter: 509 self.set_data_type() 510 511 if isinstance(self.file_location, S3Path): 512 # data = self.file_location.open('rb').read() 513 data = BytesIO(self.file_location.open("rb").read()) 514 515 else: 516 data = self.file_location 517 518 if self.data_type != "xml": 519 raise Exception("This function is only for XML peaklists (Bruker format)") 520 521 with open(data, "r") as file: 522 content = file.readlines() 523 content = "".join(content) 524 bs_content = BeautifulSoup(content, features="xml") 525 polarity = bs_content.find_all("ms_spectrum")[0]["polarity"] 526 if polarity == "-": 527 return -1 528 elif polarity == "+": 529 return +1 530 else: 531 raise Exception("Polarity %s unhandled" % polarity)
class
MassListBaseClass:
25class MassListBaseClass: 26 """The MassListBaseClass object reads mass list data types and returns the mass spectrum obj 27 28 Parameters 29 ---------- 30 file_location : Path or S3Path 31 Full data path. 32 isCentroid : bool, optional 33 Determines the mass spectrum data structure. If set to True, it assumes centroid mode. If set to False, it assumes profile mode and attempts to peak pick. Default is True. 34 analyzer : str, optional 35 The analyzer used for the mass spectrum. Default is 'Unknown'. 36 instrument_label : str, optional 37 The label of the instrument used for the mass spectrum. Default is 'Unknown'. 38 sample_name : str, optional 39 The name of the sample. Default is None. 40 header_lines : int, optional 41 The number of lines to skip in the file, including the column labels line. Default is 0. 42 isThermoProfile : bool, optional 43 Determines the number of expected columns in the file. If set to True, only m/z and intensity columns are expected. Signal-to-noise ratio (S/N) and resolving power (RP) will be calculated based on the data. Default is False. 44 headerless : bool, optional 45 If True, assumes that there are no headers present in the file (e.g., a .xy file from Bruker) and assumes two columns: m/z and intensity. Default is False. 46 47 Attributes 48 ---------- 49 parameters : DataInputSetting 50 The data input settings for the mass spectrum. 51 data_type : str 52 The type of data in the file. 53 delimiter : str 54 The delimiter used to read text-based files. 55 56 Methods 57 ------- 58 * set_parameter_from_toml(parameters_path). Sets the data input settings from a TOML file. 59 * set_parameter_from_json(parameters_path). Sets the data input settings from a JSON file. 60 * get_dataframe(). Reads the file and returns the data as a pandas DataFrame. 61 * load_settings(mass_spec_obj, output_parameters). Loads the settings for the mass spectrum. 62 * get_output_parameters(polarity, scan_index=0). Returns the output parameters for the mass spectrum. 63 * clean_data_frame(dataframe). Cleans the data frame by removing columns that are not in the expected columns set. 64 65 """ 66 67 def __init__( 68 self, 69 file_location: Path | S3Path, 70 isCentroid: bool = True, 71 analyzer: str = "Unknown", 72 instrument_label: str = "Unknown", 73 sample_name: str = None, 74 header_lines: int = 0, 75 isThermoProfile: bool = False, 76 headerless: bool = False, 77 ): 78 self.file_location = ( 79 Path(file_location) if isinstance(file_location, str) else file_location 80 ) 81 82 if not self.file_location.exists(): 83 raise FileExistsError("File does not exist: %s" % file_location) 84 85 # (newline="\n") 86 87 self.header_lines = header_lines 88 89 if isThermoProfile: 90 self._expected_columns = {Labels.mz, Labels.abundance} 91 92 else: 93 self._expected_columns = { 94 Labels.mz, 95 Labels.abundance, 96 Labels.s2n, 97 Labels.rp, 98 } 99 100 self._delimiter = None 101 102 self.isCentroid = isCentroid 103 104 self.isThermoProfile = isThermoProfile 105 106 self.headerless = headerless 107 108 self._data_type = None 109 110 self.analyzer = analyzer 111 112 self.instrument_label = instrument_label 113 114 self.sample_name = sample_name 115 116 self._parameters = deepcopy(DataInputSetting()) 117 118 @property 119 def parameters(self): 120 return self._parameters 121 122 @parameters.setter 123 def parameters(self, instance_DataInputSetting): 124 self._parameters = instance_DataInputSetting 125 126 def set_parameter_from_toml(self, parameters_path): 127 self._parameters = load_and_set_toml_parameters_class( 128 "DataInput", self.parameters, parameters_path=parameters_path 129 ) 130 131 def set_parameter_from_json(self, parameters_path): 132 self._parameters = load_and_set_parameters_class( 133 "DataInput", self.parameters, parameters_path=parameters_path 134 ) 135 136 @property 137 def data_type(self): 138 return self._data_type 139 140 @data_type.setter 141 def data_type(self, data_type): 142 self._data_type = data_type 143 144 @property 145 def delimiter(self): 146 return self._delimiter 147 148 @delimiter.setter 149 def delimiter(self, delimiter): 150 self._delimiter = delimiter 151 152 def encoding_detector(self, file_location) -> str: 153 """ 154 Detects the encoding of a file. 155 156 Parameters 157 -------- 158 file_location : str 159 The location of the file to be analyzed. 160 161 Returns 162 -------- 163 str 164 The detected encoding of the file. 165 """ 166 167 with file_location.open("rb") as rawdata: 168 result = chardet.detect(rawdata.read(10000)) 169 return result["encoding"] 170 171 def set_data_type(self): 172 """ 173 Set the data type and delimiter based on the file extension. 174 175 Raises 176 ------ 177 TypeError 178 If the data type could not be automatically recognized. 179 """ 180 if self.file_location.suffix == ".csv": 181 self.data_type = "txt" 182 self.delimiter = "," 183 elif self.file_location.suffix == ".txt": 184 self.data_type = "txt" 185 self.delimiter = "\t" 186 elif self.file_location.suffix == ".tsv": 187 self.data_type = "txt" 188 self.delimiter = "\t" 189 elif self.file_location.suffix == ".xlsx": 190 self.data_type = "excel" 191 elif self.file_location.suffix == ".ascii": 192 self.data_type = "txt" 193 self.delimiter = " " 194 elif self.file_location.suffix == ".pkl": 195 self.data_type = "dataframe" 196 elif self.file_location.suffix == ".pks": 197 self.data_type = "pks" 198 self.delimiter = " " 199 self.header_lines = 9 200 elif self.file_location.suffix == ".xml": 201 self.data_type = "xml" 202 # self.delimiter = None 203 # self.header_lines = None 204 elif self.file_location.suffix == ".xy": 205 self.data_type = "txt" 206 self.delimiter = " " 207 self.header_lines = None 208 else: 209 raise TypeError( 210 "Data type could not be automatically recognized for %s; please set data type and delimiter manually." 211 % self.file_location.name 212 ) 213 214 def get_dataframe(self) -> DataFrame: 215 """ 216 Get the data as a pandas DataFrame. 217 218 Returns 219 ------- 220 pandas.DataFrame 221 The data as a pandas DataFrame. 222 223 Raises 224 ------ 225 TypeError 226 If the data type is not supported. 227 """ 228 229 if not self.data_type or not self.delimiter: 230 self.set_data_type() 231 232 if isinstance(self.file_location, S3Path): 233 data = BytesIO(self.file_location.open("rb").read()) 234 else: 235 data = self.file_location 236 237 if self.data_type == "txt": 238 if self.headerless: 239 dataframe = read_csv( 240 data, 241 skiprows=self.header_lines, 242 delimiter=self.delimiter, 243 header=None, 244 names=["m/z", "I"], 245 encoding=self.encoding_detector(self.file_location), 246 engine="python", 247 ) 248 else: 249 dataframe = read_csv( 250 data, 251 skiprows=self.header_lines, 252 delimiter=self.delimiter, 253 encoding=self.encoding_detector(self.file_location), 254 engine="python", 255 ) 256 257 elif self.data_type == "pks": 258 # Predator .pks columns are positional: peak location (m/z), relative 259 # peak height (normalized 0-100), absolute abundance, resolving power, 260 # frequency, S/N. Use the absolute abundance as the intensity -- the 261 # relative peak height is per-spectrum normalized and not comparable 262 # across spectra, so it is named so header_translate drops it. 263 names = [ 264 "m/z", 265 "Relative Abundance", 266 "Abundance", 267 "Resolving Power", 268 "Frequency", 269 "S/N" 270 ] 271 272 273 clean_data = [] 274 with self.file_location.open() as maglabfile: 275 for i in maglabfile.readlines()[8:-1]: 276 clean_data.append(i.split()) 277 dataframe = DataFrame(clean_data, columns=names) 278 279 elif self.data_type == "dataframe": 280 dataframe = read_pickle(data) 281 282 elif self.data_type == "excel": 283 dataframe = read_excel(data) 284 285 elif self.data_type == "xml": 286 dataframe = self.read_xml_peaks(data) 287 288 else: 289 raise TypeError("Data type %s is not supported" % self.data_type) 290 291 return dataframe 292 293 def load_settings(self, mass_spec_obj, output_parameters): 294 """ 295 #TODO loading output parameters from json file is not functional 296 Load settings from a JSON file and apply them to the given mass_spec_obj. 297 298 Parameters 299 ---------- 300 mass_spec_obj : MassSpec 301 The mass spectrum object to apply the settings to. 302 303 """ 304 import json 305 import warnings 306 307 settings_file_path = self.file_location.with_suffix(".json") 308 309 if settings_file_path.exists(): 310 self._parameters = load_and_set_parameters_class( 311 "DataInput", self._parameters, parameters_path=settings_file_path 312 ) 313 314 load_and_set_parameters_ms( 315 mass_spec_obj, parameters_path=settings_file_path 316 ) 317 318 else: 319 warnings.warn( 320 "auto settings loading is enabled but could not locate the file: %s. Please load the settings manually" 321 % settings_file_path 322 ) 323 324 # TODO this will load the setting from SettingCoreMS.json 325 # coreMSHFD5 overrides this function to import the attrs stored in the h5 file 326 # loaded_settings = {} 327 # loaded_settings['MoleculaSearch'] = self.get_scan_group_attr_data(scan_index, time_index, 'MoleculaSearchSetting') 328 # loaded_settings['MassSpecPeak'] = self.get_scan_group_attr_data(scan_index, time_index, 'MassSpecPeakSetting') 329 330 # loaded_settings['MassSpectrum'] = self.get_scan_group_attr_data(scan_index, time_index, 'MassSpectrumSetting') 331 # loaded_settings['Transient'] = self.get_scan_group_attr_data(scan_index, time_index, 'TransientSetting') 332 333 def get_output_parameters(self, polarity: int, scan_index: int = 0) -> dict: 334 """ 335 Get the output parameters for the mass spectrum. 336 337 Parameters 338 ---------- 339 polarity : int 340 The polarity of the mass spectrum +1 or -1. 341 scan_index : int, optional 342 The index of the scan. Default is 0. 343 344 Returns 345 ------- 346 dict 347 A dictionary containing the output parameters. 348 349 """ 350 from copy import deepcopy 351 352 output_parameters = default_parameters(self.file_location) 353 354 if self.isCentroid: 355 output_parameters["label"] = Labels.corems_centroid 356 else: 357 output_parameters["label"] = Labels.bruker_profile 358 359 output_parameters["analyzer"] = self.analyzer 360 361 output_parameters["instrument_label"] = self.instrument_label 362 363 output_parameters["sample_name"] = self.sample_name 364 365 output_parameters["Aterm"] = None 366 367 output_parameters["Bterm"] = None 368 369 output_parameters["Cterm"] = None 370 371 output_parameters["polarity"] = polarity 372 373 # scan_number and rt will be need to lc ms==== 374 375 output_parameters["mobility_scan"] = 0 376 377 output_parameters["mobility_rt"] = 0 378 379 output_parameters["scan_number"] = scan_index 380 381 output_parameters["rt"] = 0 382 383 return output_parameters 384 385 def clean_data_frame(self, dataframe): 386 """ 387 Clean the input dataframe by removing columns that are not expected. 388 389 Parameters 390 ---------- 391 pandas.DataFrame 392 The input dataframe to be cleaned. 393 394 """ 395 396 for column_name in dataframe.columns: 397 expected_column_name = self.parameters.header_translate.get(column_name) 398 if expected_column_name not in self._expected_columns: 399 del dataframe[column_name] 400 401 def check_columns(self, header_labels: list[str]): 402 """ 403 Check if the given header labels match the expected columns. 404 405 Parameters 406 ---------- 407 header_labels : list 408 The header labels to be checked. 409 410 Raises 411 ------ 412 Exception 413 If any expected column is not found in the header labels. 414 """ 415 found_label = set() 416 417 for label in header_labels: 418 if not label in self._expected_columns: 419 user_column_name = self.parameters.header_translate.get(label) 420 if user_column_name in self._expected_columns: 421 found_label.add(user_column_name) 422 else: 423 found_label.add(label) 424 425 not_found = self._expected_columns - found_label 426 427 if len(not_found) > 0: 428 raise Exception( 429 "Please make sure to include the columns %s" % ", ".join(not_found) 430 ) 431 432 def read_xml_peaks(self, data: str) -> DataFrame: 433 """ 434 Read peaks from a Bruker .xml file and return a pandas DataFrame. 435 436 Parameters 437 ---------- 438 data : str 439 The path to the .xml file. 440 441 Returns 442 ------- 443 pandas.DataFrame 444 A DataFrame containing the peak data with columns: 'm/z', 'I', 'Resolving Power', 'Area', 'S/N', 'fwhm'. 445 """ 446 from numpy import nan 447 448 with open(data, "r") as file: 449 content = file.readlines() 450 content = "".join(content) 451 bs_content = BeautifulSoup(content, features="xml") 452 peaks_xml = bs_content.find_all("pk") 453 454 # initialise lists of the peak variables 455 areas = [] 456 fwhms = [] 457 intensities = [] 458 mzs = [] 459 res = [] 460 sn = [] 461 # iterate through the peaks appending to each list 462 for peak in peaks_xml: 463 areas.append( 464 float(peak.get("a", nan)) 465 ) # Use a default value if key 'a' is missing 466 fwhms.append( 467 float(peak.get("fwhm", nan)) 468 ) # Use a default value if key 'fwhm' is missing 469 intensities.append( 470 float(peak.get("i", nan)) 471 ) # Use a default value if key 'i' is missing 472 mzs.append( 473 float(peak.get("mz", nan)) 474 ) # Use a default value if key 'mz' is missing 475 res.append( 476 float(peak.get("res", nan)) 477 ) # Use a default value if key 'res' is missing 478 sn.append( 479 float(peak.get("sn", nan)) 480 ) # Use a default value if key 'sn' is missing 481 482 # Compile pandas dataframe of these values 483 names = ["m/z", "I", "Resolving Power", "Area", "S/N", "fwhm"] 484 df = DataFrame(columns=names, dtype=float) 485 df["m/z"] = mzs 486 df["I"] = intensities 487 df["Resolving Power"] = res 488 df["Area"] = areas 489 df["S/N"] = sn 490 df["fwhm"] = fwhms 491 return df 492 493 def get_xml_polarity(self): 494 """ 495 Get the polarity from an XML peaklist. 496 497 Returns 498 ------- 499 int 500 The polarity of the XML peaklist. Returns -1 for negative polarity, +1 for positive polarity. 501 502 Raises 503 ------ 504 Exception 505 If the data type is not XML peaklist in Bruker format or if the polarity is unhandled. 506 """ 507 508 # Check its an actual xml 509 if not self.data_type or not self.delimiter: 510 self.set_data_type() 511 512 if isinstance(self.file_location, S3Path): 513 # data = self.file_location.open('rb').read() 514 data = BytesIO(self.file_location.open("rb").read()) 515 516 else: 517 data = self.file_location 518 519 if self.data_type != "xml": 520 raise Exception("This function is only for XML peaklists (Bruker format)") 521 522 with open(data, "r") as file: 523 content = file.readlines() 524 content = "".join(content) 525 bs_content = BeautifulSoup(content, features="xml") 526 polarity = bs_content.find_all("ms_spectrum")[0]["polarity"] 527 if polarity == "-": 528 return -1 529 elif polarity == "+": 530 return +1 531 else: 532 raise Exception("Polarity %s unhandled" % polarity)
The MassListBaseClass object reads mass list data types and returns the mass spectrum obj
Parameters
- file_location (Path or S3Path): Full data path.
- isCentroid (bool, optional): Determines the mass spectrum data structure. If set to True, it assumes centroid mode. If set to False, it assumes profile mode and attempts to peak pick. Default is True.
- analyzer (str, optional): The analyzer used for the mass spectrum. Default is 'Unknown'.
- instrument_label (str, optional): The label of the instrument used for the mass spectrum. Default is 'Unknown'.
- sample_name (str, optional): The name of the sample. Default is None.
- header_lines (int, optional): The number of lines to skip in the file, including the column labels line. Default is 0.
- isThermoProfile (bool, optional): Determines the number of expected columns in the file. If set to True, only m/z and intensity columns are expected. Signal-to-noise ratio (S/N) and resolving power (RP) will be calculated based on the data. Default is False.
- headerless (bool, optional): If True, assumes that there are no headers present in the file (e.g., a .xy file from Bruker) and assumes two columns: m/z and intensity. Default is False.
Attributes
- parameters (DataInputSetting): The data input settings for the mass spectrum.
- data_type (str): The type of data in the file.
- delimiter (str): The delimiter used to read text-based files.
Methods
- set_parameter_from_toml(parameters_path). Sets the data input settings from a TOML file.
- set_parameter_from_json(parameters_path). Sets the data input settings from a JSON file.
- get_dataframe(). Reads the file and returns the data as a pandas DataFrame.
- load_settings(mass_spec_obj, output_parameters). Loads the settings for the mass spectrum.
- get_output_parameters(polarity, scan_index=0). Returns the output parameters for the mass spectrum.
- clean_data_frame(dataframe). Cleans the data frame by removing columns that are not in the expected columns set.
MassListBaseClass( file_location: pathlib._local.Path | s3path.current_version.S3Path, isCentroid: bool = True, analyzer: str = 'Unknown', instrument_label: str = 'Unknown', sample_name: str = None, header_lines: int = 0, isThermoProfile: bool = False, headerless: bool = False)
67 def __init__( 68 self, 69 file_location: Path | S3Path, 70 isCentroid: bool = True, 71 analyzer: str = "Unknown", 72 instrument_label: str = "Unknown", 73 sample_name: str = None, 74 header_lines: int = 0, 75 isThermoProfile: bool = False, 76 headerless: bool = False, 77 ): 78 self.file_location = ( 79 Path(file_location) if isinstance(file_location, str) else file_location 80 ) 81 82 if not self.file_location.exists(): 83 raise FileExistsError("File does not exist: %s" % file_location) 84 85 # (newline="\n") 86 87 self.header_lines = header_lines 88 89 if isThermoProfile: 90 self._expected_columns = {Labels.mz, Labels.abundance} 91 92 else: 93 self._expected_columns = { 94 Labels.mz, 95 Labels.abundance, 96 Labels.s2n, 97 Labels.rp, 98 } 99 100 self._delimiter = None 101 102 self.isCentroid = isCentroid 103 104 self.isThermoProfile = isThermoProfile 105 106 self.headerless = headerless 107 108 self._data_type = None 109 110 self.analyzer = analyzer 111 112 self.instrument_label = instrument_label 113 114 self.sample_name = sample_name 115 116 self._parameters = deepcopy(DataInputSetting())
def
encoding_detector(self, file_location) -> str:
152 def encoding_detector(self, file_location) -> str: 153 """ 154 Detects the encoding of a file. 155 156 Parameters 157 -------- 158 file_location : str 159 The location of the file to be analyzed. 160 161 Returns 162 -------- 163 str 164 The detected encoding of the file. 165 """ 166 167 with file_location.open("rb") as rawdata: 168 result = chardet.detect(rawdata.read(10000)) 169 return result["encoding"]
Detects the encoding of a file.
Parameters
- file_location (str): The location of the file to be analyzed.
Returns
- str: The detected encoding of the file.
def
set_data_type(self):
171 def set_data_type(self): 172 """ 173 Set the data type and delimiter based on the file extension. 174 175 Raises 176 ------ 177 TypeError 178 If the data type could not be automatically recognized. 179 """ 180 if self.file_location.suffix == ".csv": 181 self.data_type = "txt" 182 self.delimiter = "," 183 elif self.file_location.suffix == ".txt": 184 self.data_type = "txt" 185 self.delimiter = "\t" 186 elif self.file_location.suffix == ".tsv": 187 self.data_type = "txt" 188 self.delimiter = "\t" 189 elif self.file_location.suffix == ".xlsx": 190 self.data_type = "excel" 191 elif self.file_location.suffix == ".ascii": 192 self.data_type = "txt" 193 self.delimiter = " " 194 elif self.file_location.suffix == ".pkl": 195 self.data_type = "dataframe" 196 elif self.file_location.suffix == ".pks": 197 self.data_type = "pks" 198 self.delimiter = " " 199 self.header_lines = 9 200 elif self.file_location.suffix == ".xml": 201 self.data_type = "xml" 202 # self.delimiter = None 203 # self.header_lines = None 204 elif self.file_location.suffix == ".xy": 205 self.data_type = "txt" 206 self.delimiter = " " 207 self.header_lines = None 208 else: 209 raise TypeError( 210 "Data type could not be automatically recognized for %s; please set data type and delimiter manually." 211 % self.file_location.name 212 )
Set the data type and delimiter based on the file extension.
Raises
- TypeError: If the data type could not be automatically recognized.
def
get_dataframe(self) -> pandas.DataFrame:
214 def get_dataframe(self) -> DataFrame: 215 """ 216 Get the data as a pandas DataFrame. 217 218 Returns 219 ------- 220 pandas.DataFrame 221 The data as a pandas DataFrame. 222 223 Raises 224 ------ 225 TypeError 226 If the data type is not supported. 227 """ 228 229 if not self.data_type or not self.delimiter: 230 self.set_data_type() 231 232 if isinstance(self.file_location, S3Path): 233 data = BytesIO(self.file_location.open("rb").read()) 234 else: 235 data = self.file_location 236 237 if self.data_type == "txt": 238 if self.headerless: 239 dataframe = read_csv( 240 data, 241 skiprows=self.header_lines, 242 delimiter=self.delimiter, 243 header=None, 244 names=["m/z", "I"], 245 encoding=self.encoding_detector(self.file_location), 246 engine="python", 247 ) 248 else: 249 dataframe = read_csv( 250 data, 251 skiprows=self.header_lines, 252 delimiter=self.delimiter, 253 encoding=self.encoding_detector(self.file_location), 254 engine="python", 255 ) 256 257 elif self.data_type == "pks": 258 # Predator .pks columns are positional: peak location (m/z), relative 259 # peak height (normalized 0-100), absolute abundance, resolving power, 260 # frequency, S/N. Use the absolute abundance as the intensity -- the 261 # relative peak height is per-spectrum normalized and not comparable 262 # across spectra, so it is named so header_translate drops it. 263 names = [ 264 "m/z", 265 "Relative Abundance", 266 "Abundance", 267 "Resolving Power", 268 "Frequency", 269 "S/N" 270 ] 271 272 273 clean_data = [] 274 with self.file_location.open() as maglabfile: 275 for i in maglabfile.readlines()[8:-1]: 276 clean_data.append(i.split()) 277 dataframe = DataFrame(clean_data, columns=names) 278 279 elif self.data_type == "dataframe": 280 dataframe = read_pickle(data) 281 282 elif self.data_type == "excel": 283 dataframe = read_excel(data) 284 285 elif self.data_type == "xml": 286 dataframe = self.read_xml_peaks(data) 287 288 else: 289 raise TypeError("Data type %s is not supported" % self.data_type) 290 291 return dataframe
Get the data as a pandas DataFrame.
Returns
- pandas.DataFrame: The data as a pandas DataFrame.
Raises
- TypeError: If the data type is not supported.
def
load_settings(self, mass_spec_obj, output_parameters):
293 def load_settings(self, mass_spec_obj, output_parameters): 294 """ 295 #TODO loading output parameters from json file is not functional 296 Load settings from a JSON file and apply them to the given mass_spec_obj. 297 298 Parameters 299 ---------- 300 mass_spec_obj : MassSpec 301 The mass spectrum object to apply the settings to. 302 303 """ 304 import json 305 import warnings 306 307 settings_file_path = self.file_location.with_suffix(".json") 308 309 if settings_file_path.exists(): 310 self._parameters = load_and_set_parameters_class( 311 "DataInput", self._parameters, parameters_path=settings_file_path 312 ) 313 314 load_and_set_parameters_ms( 315 mass_spec_obj, parameters_path=settings_file_path 316 ) 317 318 else: 319 warnings.warn( 320 "auto settings loading is enabled but could not locate the file: %s. Please load the settings manually" 321 % settings_file_path 322 ) 323 324 # TODO this will load the setting from SettingCoreMS.json 325 # coreMSHFD5 overrides this function to import the attrs stored in the h5 file 326 # loaded_settings = {} 327 # loaded_settings['MoleculaSearch'] = self.get_scan_group_attr_data(scan_index, time_index, 'MoleculaSearchSetting') 328 # loaded_settings['MassSpecPeak'] = self.get_scan_group_attr_data(scan_index, time_index, 'MassSpecPeakSetting') 329 330 # loaded_settings['MassSpectrum'] = self.get_scan_group_attr_data(scan_index, time_index, 'MassSpectrumSetting') 331 # loaded_settings['Transient'] = self.get_scan_group_attr_data(scan_index, time_index, 'TransientSetting')
TODO loading output parameters from json file is not functional
Load settings from a JSON file and apply them to the given mass_spec_obj.
Parameters
- mass_spec_obj (MassSpec): The mass spectrum object to apply the settings to.
def
get_output_parameters(self, polarity: int, scan_index: int = 0) -> dict:
333 def get_output_parameters(self, polarity: int, scan_index: int = 0) -> dict: 334 """ 335 Get the output parameters for the mass spectrum. 336 337 Parameters 338 ---------- 339 polarity : int 340 The polarity of the mass spectrum +1 or -1. 341 scan_index : int, optional 342 The index of the scan. Default is 0. 343 344 Returns 345 ------- 346 dict 347 A dictionary containing the output parameters. 348 349 """ 350 from copy import deepcopy 351 352 output_parameters = default_parameters(self.file_location) 353 354 if self.isCentroid: 355 output_parameters["label"] = Labels.corems_centroid 356 else: 357 output_parameters["label"] = Labels.bruker_profile 358 359 output_parameters["analyzer"] = self.analyzer 360 361 output_parameters["instrument_label"] = self.instrument_label 362 363 output_parameters["sample_name"] = self.sample_name 364 365 output_parameters["Aterm"] = None 366 367 output_parameters["Bterm"] = None 368 369 output_parameters["Cterm"] = None 370 371 output_parameters["polarity"] = polarity 372 373 # scan_number and rt will be need to lc ms==== 374 375 output_parameters["mobility_scan"] = 0 376 377 output_parameters["mobility_rt"] = 0 378 379 output_parameters["scan_number"] = scan_index 380 381 output_parameters["rt"] = 0 382 383 return output_parameters
Get the output parameters for the mass spectrum.
Parameters
- polarity (int): The polarity of the mass spectrum +1 or -1.
- scan_index (int, optional): The index of the scan. Default is 0.
Returns
- dict: A dictionary containing the output parameters.
def
clean_data_frame(self, dataframe):
385 def clean_data_frame(self, dataframe): 386 """ 387 Clean the input dataframe by removing columns that are not expected. 388 389 Parameters 390 ---------- 391 pandas.DataFrame 392 The input dataframe to be cleaned. 393 394 """ 395 396 for column_name in dataframe.columns: 397 expected_column_name = self.parameters.header_translate.get(column_name) 398 if expected_column_name not in self._expected_columns: 399 del dataframe[column_name]
Clean the input dataframe by removing columns that are not expected.
Parameters
- pandas.DataFrame: The input dataframe to be cleaned.
def
check_columns(self, header_labels: list[str]):
401 def check_columns(self, header_labels: list[str]): 402 """ 403 Check if the given header labels match the expected columns. 404 405 Parameters 406 ---------- 407 header_labels : list 408 The header labels to be checked. 409 410 Raises 411 ------ 412 Exception 413 If any expected column is not found in the header labels. 414 """ 415 found_label = set() 416 417 for label in header_labels: 418 if not label in self._expected_columns: 419 user_column_name = self.parameters.header_translate.get(label) 420 if user_column_name in self._expected_columns: 421 found_label.add(user_column_name) 422 else: 423 found_label.add(label) 424 425 not_found = self._expected_columns - found_label 426 427 if len(not_found) > 0: 428 raise Exception( 429 "Please make sure to include the columns %s" % ", ".join(not_found) 430 )
Check if the given header labels match the expected columns.
Parameters
- header_labels (list): The header labels to be checked.
Raises
- Exception: If any expected column is not found in the header labels.
def
read_xml_peaks(self, data: str) -> pandas.DataFrame:
432 def read_xml_peaks(self, data: str) -> DataFrame: 433 """ 434 Read peaks from a Bruker .xml file and return a pandas DataFrame. 435 436 Parameters 437 ---------- 438 data : str 439 The path to the .xml file. 440 441 Returns 442 ------- 443 pandas.DataFrame 444 A DataFrame containing the peak data with columns: 'm/z', 'I', 'Resolving Power', 'Area', 'S/N', 'fwhm'. 445 """ 446 from numpy import nan 447 448 with open(data, "r") as file: 449 content = file.readlines() 450 content = "".join(content) 451 bs_content = BeautifulSoup(content, features="xml") 452 peaks_xml = bs_content.find_all("pk") 453 454 # initialise lists of the peak variables 455 areas = [] 456 fwhms = [] 457 intensities = [] 458 mzs = [] 459 res = [] 460 sn = [] 461 # iterate through the peaks appending to each list 462 for peak in peaks_xml: 463 areas.append( 464 float(peak.get("a", nan)) 465 ) # Use a default value if key 'a' is missing 466 fwhms.append( 467 float(peak.get("fwhm", nan)) 468 ) # Use a default value if key 'fwhm' is missing 469 intensities.append( 470 float(peak.get("i", nan)) 471 ) # Use a default value if key 'i' is missing 472 mzs.append( 473 float(peak.get("mz", nan)) 474 ) # Use a default value if key 'mz' is missing 475 res.append( 476 float(peak.get("res", nan)) 477 ) # Use a default value if key 'res' is missing 478 sn.append( 479 float(peak.get("sn", nan)) 480 ) # Use a default value if key 'sn' is missing 481 482 # Compile pandas dataframe of these values 483 names = ["m/z", "I", "Resolving Power", "Area", "S/N", "fwhm"] 484 df = DataFrame(columns=names, dtype=float) 485 df["m/z"] = mzs 486 df["I"] = intensities 487 df["Resolving Power"] = res 488 df["Area"] = areas 489 df["S/N"] = sn 490 df["fwhm"] = fwhms 491 return df
Read peaks from a Bruker .xml file and return a pandas DataFrame.
Parameters
- data (str): The path to the .xml file.
Returns
- pandas.DataFrame: A DataFrame containing the peak data with columns: 'm/z', 'I', 'Resolving Power', 'Area', 'S/N', 'fwhm'.
def
get_xml_polarity(self):
493 def get_xml_polarity(self): 494 """ 495 Get the polarity from an XML peaklist. 496 497 Returns 498 ------- 499 int 500 The polarity of the XML peaklist. Returns -1 for negative polarity, +1 for positive polarity. 501 502 Raises 503 ------ 504 Exception 505 If the data type is not XML peaklist in Bruker format or if the polarity is unhandled. 506 """ 507 508 # Check its an actual xml 509 if not self.data_type or not self.delimiter: 510 self.set_data_type() 511 512 if isinstance(self.file_location, S3Path): 513 # data = self.file_location.open('rb').read() 514 data = BytesIO(self.file_location.open("rb").read()) 515 516 else: 517 data = self.file_location 518 519 if self.data_type != "xml": 520 raise Exception("This function is only for XML peaklists (Bruker format)") 521 522 with open(data, "r") as file: 523 content = file.readlines() 524 content = "".join(content) 525 bs_content = BeautifulSoup(content, features="xml") 526 polarity = bs_content.find_all("ms_spectrum")[0]["polarity"] 527 if polarity == "-": 528 return -1 529 elif polarity == "+": 530 return +1 531 else: 532 raise Exception("Polarity %s unhandled" % polarity)
Get the polarity from an XML peaklist.
Returns
- int: The polarity of the XML peaklist. Returns -1 for negative polarity, +1 for positive polarity.
Raises
- Exception: If the data type is not XML peaklist in Bruker format or if the polarity is unhandled.