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