corems.mass_spectrum.output.export

   1__author__ = "Yuri E. Corilo"
   2__date__ = "Nov 11, 2019"
   3
   4import json
   5from datetime import datetime, timezone
   6from pathlib import Path
   7from threading import Thread
   8
   9import h5py
  10import numpy as np
  11from numpy import nan as NaN
  12from pandas import DataFrame
  13
  14# Labels is referenced only inside eval() below. Ruff cannot see that use.
  15from corems.encapsulation.constant import (
  16    Atoms,
  17    DEFAULT_HDF5_COMPRESSION,
  18    Labels,  # noqa: F401
  19)
  20from corems.encapsulation.output import parameter_to_dict
  21from corems.encapsulation.output.parameter_to_json import _dumps_toml
  22from corems.mass_spectrum.factory.MassSpectrumClasses import MassSpecfromFreq
  23
  24
  25def hdf5_dataset_kwargs(shape, compression=DEFAULT_HDF5_COMPRESSION, vlen_str=False):
  26    """Build ``h5py.Group.create_dataset`` filter kwargs for a compression choice.
  27
  28    Centralizes the HDF5 writers' compression choice (one helper used by every
  29    ``create_dataset`` call on the mass spectrum / LC-MS export path) and the
  30    filter exceptions h5py enforces: it refuses chunk/filter options on scalar
  31    (``shape == ()``) or zero-length datasets, and the fletcher32 checksum
  32    filter gains nothing (and h5py can reject it) on variable-length string
  33    datasets.
  34
  35    Parameters
  36    ----------
  37    shape : tuple
  38        Shape of the data about to be written.
  39    compression : int, str or None, optional
  40        A gzip level 0-9, ``"lzf"``, or ``None``/``"none"`` for no
  41        compression. Default is 4. ``"lzf"`` is h5py's own filter, not part
  42        of core HDF5 -- a file written with it needs h5py (or another tool
  43        with the lzf plugin available) to read the compressed datasets
  44        back; gzip is the portable choice across other HDF5 tools.
  45    vlen_str : bool, optional
  46        True when the dataset's dtype is a variable-length string
  47        (``h5py.string_dtype()``); skips the shuffle/fletcher32 filters that
  48        don't apply to vlen data. Default is False.
  49
  50    Returns
  51    -------
  52    dict
  53        Keyword arguments to splat into ``create_dataset`` (besides ``data``
  54        and ``dtype``).
  55    """
  56    if not shape or 0 in shape:
  57        return {}
  58
  59    kwargs = {"chunks": True}
  60
  61    if compression not in (None, "none"):
  62        if compression == "lzf":
  63            kwargs["compression"] = "lzf"
  64        else:
  65            kwargs["compression"] = "gzip"
  66            kwargs["compression_opts"] = int(compression)
  67
  68    if not vlen_str:
  69        kwargs["shuffle"] = True
  70        kwargs["fletcher32"] = True
  71
  72    return kwargs
  73
  74
  75class HighResMassSpecExport(Thread):
  76    """A class for exporting high-resolution mass spectra.
  77
  78    Parameters
  79    ----------
  80    out_file_path : str
  81        The output file path.
  82    mass_spectrum : MassSpectrum
  83        The mass spectrum to export.
  84    output_type : str, optional
  85        The type of output file. Defaults to 'excel'. Can be 'excel', 'csv', 'pandas', 'parquet' or 'hdf5'.
  86
  87    Attributes
  88    ----------
  89    output_file : Path
  90        The output file path.
  91    output_type : str
  92        The type of output file.
  93    mass_spectrum : MassSpectrum
  94        The mass spectrum to export.
  95    atoms_order_list : list
  96        The list of assigned atoms in the order specified by Atoms.atoms_order list.
  97    columns_label : list
  98        The column labels in order.
  99
 100    Methods
 101    -------
 102    * save().
 103        Save the mass spectrum data to the output file.
 104    * run().
 105        Run the export process.
 106    * get_pandas_df().
 107        Returns the mass spectrum data as a pandas DataFrame.
 108    * write_settings(output_path, mass_spectrum).
 109        Writes the settings of the mass spectrum to a JSON file.
 110    * to_pandas(write_metadata=True).
 111        Exports the mass spectrum data to a pandas DataFrame and saves it as a pickle file.
 112    * to_excel(write_metadata=True).
 113        Exports the mass spectrum data to an Excel file.
 114    * to_csv(write_metadata=True).
 115        Exports the mass spectrum data to a CSV file.
 116    * to_parquet(write_metadata=True).
 117        Exports the mass spectrum data to a Parquet file.
 118    * to_json().
 119        Exports the mass spectrum data to a JSON string.
 120    * to_hdf().
 121        Exports the mass spectrum data to an HDF5 file.
 122    * parameters_to_toml().
 123        Converts the mass spectrum parameters to a TOML string.
 124    * parameters_to_json().
 125        Converts the mass spectrum parameters to a JSON string.
 126    * get_mass_spec_attrs(mass_spectrum).
 127        Returns the mass spectrum attributes as a dictionary.
 128    * get_all_used_atoms_in_order(mass_spectrum).
 129        Returns the list of assigned atoms in the order specified by Atoms.atoms_order list.
 130    * list_dict_to_list(mass_spectrum, is_hdf5=False).
 131        Returns the mass spectrum data as a list of dictionaries.
 132    * get_list_dict_data(mass_spectrum, include_no_match=True, include_isotopologues=True, isotopologue_inline=True, no_match_inline=False, is_hdf5=False).
 133        Returns the mass spectrum data as a list of dictionaries.
 134
 135    """
 136
 137    def __init__(self, out_file_path, mass_spectrum, output_type="excel"):
 138        Thread.__init__(self)
 139
 140        self.output_file = Path(out_file_path)
 141
 142        # 'excel', 'csv' or 'pandas'
 143        self.output_type = output_type
 144
 145        self.mass_spectrum = mass_spectrum
 146
 147        # collect all assigned atoms and order them accordingly to the Atoms.atoms_order list
 148        self.atoms_order_list = self.get_all_used_atoms_in_order(self.mass_spectrum)
 149
 150        self._init_columns()
 151
 152    def _init_columns(self):
 153        """Initialize the columns for the mass spectrum output."""
 154        # column labels in order
 155        self.columns_label = [
 156            "Index",
 157            "m/z",
 158            "Calibrated m/z",
 159            "Calculated m/z",
 160            "Peak Height",
 161            "Peak Area",
 162            "Resolving Power",
 163            "S/N",
 164            "Ion Charge",
 165            "m/z Error (ppm)",
 166            "m/z Error Score",
 167            "Isotopologue Similarity",
 168            "Confidence Score",
 169            "DBE",
 170            "O/C",
 171            "H/C",
 172            "Heteroatom Class",
 173            "Ion Type",
 174            "Adduct",
 175            "Is Isotopologue",
 176            "Mono Isotopic Index",
 177            "Molecular Formula",
 178        ]
 179
 180    @property
 181    def output_type(self):
 182        """Returns the output type of the mass spectrum."""
 183        return self._output_type
 184
 185    @output_type.setter
 186    def output_type(self, output_type):
 187        output_types = ["excel", "csv", "pandas", "parquet", "hdf5"]
 188        if output_type in output_types:
 189            self._output_type = output_type
 190        else:
 191            raise TypeError(
 192                'Supported types are "excel", "csv", "pandas", "parquet" or "hdf5", %s entered'
 193                % output_type
 194            )
 195
 196    def save(self):
 197        """Save the mass spectrum data to the output file.
 198
 199        Raises
 200        ------
 201        ValueError
 202            If the output type is not supported.
 203        """
 204
 205        if self.output_type == "excel":
 206            self.to_excel()
 207        elif self.output_type == "csv":
 208            self.to_csv()
 209        elif self.output_type == "pandas":
 210            self.to_pandas()
 211        elif self.output_type == "parquet":
 212            self.to_parquet()
 213        elif self.output_type == "hdf5":
 214            self.to_hdf()
 215        else:
 216            raise ValueError(
 217                "Unkown output type: %s; it can be 'excel', 'csv', 'pandas', 'parquet' or 'hdf5'"
 218                % self.output_type
 219            )
 220
 221    def run(self):
 222        """Run the export process.
 223
 224        This method is called when the thread starts.
 225        It calls the save method to perform the export."""
 226        self.save()
 227
 228    def get_pandas_df(self, additional_columns=None):
 229        """Returns the mass spectrum data as a pandas DataFrame.
 230
 231        Parameters
 232        ----------
 233        additional_columns : list, optional
 234            Additional columns to include in the DataFrame. Defaults to None.
 235            Suitable additional columns are: 'Aromaticity Index', 'NOSC', 'Aromaticity Index (modified)'.
 236
 237        Returns
 238        -------
 239        DataFrame
 240            The mass spectrum data as a pandas DataFrame.
 241        """
 242        if additional_columns is not None:
 243            possible_additional_columns = [
 244                "Aromaticity Index",
 245                "NOSC",
 246                "Aromaticity Index (modified)",
 247            ]
 248            if additional_columns:
 249                for column in additional_columns:
 250                    if column not in possible_additional_columns:
 251                        raise ValueError("Invalid additional column: %s" % column)
 252            columns = (
 253                self.columns_label
 254                + additional_columns
 255                + self.get_all_used_atoms_in_order(self.mass_spectrum)
 256            )
 257        else:
 258            columns = self.columns_label + self.get_all_used_atoms_in_order(
 259                self.mass_spectrum
 260            )
 261        dict_data_list = self.get_list_dict_data(
 262            self.mass_spectrum, additional_columns=additional_columns
 263        )
 264        df = DataFrame(dict_data_list, columns=columns)
 265        df.attrs["name"] = self.output_file
 266        return df
 267
 268    def write_settings(self, output_path, mass_spectrum):
 269        """Writes the settings of the mass spectrum to a JSON file.
 270
 271        Parameters
 272        ----------
 273        output_path : str
 274            The output file path.
 275        mass_spectrum : MassSpectrum
 276            The mass spectrum to export.
 277        """
 278
 279        import json
 280
 281        dict_setting = parameter_to_dict.get_dict_data_ms(mass_spectrum)
 282
 283        dict_setting["MassSpecAttrs"] = self.get_mass_spec_attrs(mass_spectrum)
 284        dict_setting["analyzer"] = mass_spectrum.analyzer
 285        dict_setting["instrument_label"] = mass_spectrum.instrument_label
 286        dict_setting["sample_name"] = mass_spectrum.sample_name
 287
 288        with open(
 289            output_path.with_suffix(".json"),
 290            "w",
 291            encoding="utf8",
 292        ) as outfile:
 293            output = json.dumps(
 294                dict_setting, sort_keys=True, indent=4, separators=(",", ": ")
 295            )
 296            outfile.write(output)
 297
 298    def to_pandas(self, write_metadata=True):
 299        """Exports the mass spectrum data to a pandas DataFrame and saves it as a pickle file.
 300
 301        Parameters
 302        ----------
 303        write_metadata : bool, optional
 304            Whether to write the metadata to a JSON file. Defaults to True.
 305        """
 306
 307        columns = self.columns_label + self.get_all_used_atoms_in_order(
 308            self.mass_spectrum
 309        )
 310
 311        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
 312
 313        df = DataFrame(dict_data_list, columns=columns)
 314
 315        df.to_pickle(self.output_file.with_suffix(".pkl"))
 316
 317        if write_metadata:
 318            self.write_settings(self.output_file, self.mass_spectrum)
 319
 320    def to_excel(self, write_metadata=True):
 321        """Exports the mass spectrum data to an Excel file.
 322
 323        Parameters
 324        ----------
 325        write_metadata : bool, optional
 326            Whether to write the metadata to a JSON file. Defaults to True.
 327        """
 328
 329        columns = self.columns_label + self.get_all_used_atoms_in_order(
 330            self.mass_spectrum
 331        )
 332
 333        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
 334
 335        df = DataFrame(dict_data_list, columns=columns)
 336
 337        df.to_excel(self.output_file.with_suffix(".xlsx"))
 338
 339        if write_metadata:
 340            self.write_settings(self.output_file, self.mass_spectrum)
 341
 342    def to_csv(self, write_metadata=True):
 343        """Exports the mass spectrum data to a CSV file.
 344
 345        Parameters
 346        ----------
 347        write_metadata : bool, optional
 348            Whether to write the metadata to a JSON file. Defaults to True.
 349        """
 350
 351        columns = self.columns_label + self.get_all_used_atoms_in_order(
 352            self.mass_spectrum
 353        )
 354
 355        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
 356
 357        import csv
 358
 359        try:
 360            with open(self.output_file.with_suffix(".csv"), "w", newline="") as csvfile:
 361                writer = csv.DictWriter(csvfile, fieldnames=columns)
 362                writer.writeheader()
 363                for data in dict_data_list:
 364                    writer.writerow(data)
 365            if write_metadata:
 366                self.write_settings(self.output_file, self.mass_spectrum)
 367
 368        except IOError as ioerror:
 369            print(ioerror)
 370
 371    def to_parquet(self, write_metadata=True):
 372        """Exports the mass spectrum data to a Parquet file.
 373
 374        Parameters
 375        ----------
 376        write_metadata : bool, optional
 377            Whether to write the metadata to a JSON file. Defaults to True.
 378
 379        Notes
 380        -----
 381        Requires ``pyarrow`` (or ``fastparquet``) for pandas Parquet I/O.
 382        """
 383
 384        columns = self.columns_label + self.get_all_used_atoms_in_order(
 385            self.mass_spectrum
 386        )
 387
 388        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
 389
 390        df = DataFrame(dict_data_list, columns=columns)
 391
 392        df.to_parquet(self.output_file.with_suffix(".parquet"), index=False)
 393
 394        if write_metadata:
 395            self.write_settings(self.output_file, self.mass_spectrum)
 396
 397    def to_json(self):
 398        """Exports the mass spectrum data to a JSON string."""
 399
 400        columns = self.columns_label + self.get_all_used_atoms_in_order(
 401            self.mass_spectrum
 402        )
 403
 404        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
 405
 406        df = DataFrame(dict_data_list, columns=columns)
 407
 408        # for key, values in dict_data.items():
 409        #    if not values: dict_data[key] = NaN
 410
 411        # output = json.dumps(dict_data, sort_keys=True, indent=4, separators=(',', ': '))
 412        return df.to_json(orient="records")
 413
 414    def add_mass_spectrum_to_hdf5(
 415        self,
 416        hdf_handle,
 417        mass_spectrum,
 418        group_key,
 419        mass_spectra_group=None,
 420        export_raw=True,
 421        compression=DEFAULT_HDF5_COMPRESSION,
 422        profile_dtype="f4",
 423    ):
 424        """Adds the mass spectrum data to an HDF5 file.
 425
 426        Parameters
 427        ----------
 428        hdf_handle : h5py.File
 429            The HDF5 file handle.
 430        mass_spectrum : MassSpectrum
 431            The mass spectrum to add to the HDF5 file.
 432        group_key : str
 433            The group key (where to add the mass spectrum data within the HDF5 file).
 434        mass_spectra_group : h5py.Group, optional
 435            The mass spectra group. Defaults to None (no group, mass spectrum is added to the root).
 436        export_raw : bool, optional
 437            Whether to export the raw data. Defaults to True.
 438            If False, only the processed data (peaks) is exported (essentially centroided data).
 439        compression : int, str or None, optional
 440            Compression for the written datasets: a gzip level 0-9, ``"lzf"``,
 441            or ``None``/``"none"``. Default is 4. ``"lzf"`` is h5py's own
 442            filter, not a standard HDF5 one -- it needs h5py (or a tool
 443            with the lzf plugin) to read the result back.
 444        profile_dtype : str, optional
 445            dtype used for the stored profile m/z/abundance arrays
 446            (``"f4"`` or ``"f8"``). Default is ``"f4"``.
 447        """
 448        if mass_spectra_group is None:
 449            # Check if the file has the necessary attributes and add them if not
 450            # This assumes that if there is a mass_spectra_group, these attributes were already added to the file
 451            if not hdf_handle.attrs.get("date_utc"):
 452                timenow = str(
 453                    datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z")
 454                )
 455                hdf_handle.attrs["date_utc"] = timenow
 456                hdf_handle.attrs["file_name"] = mass_spectrum.filename.name
 457                hdf_handle.attrs["data_structure"] = "mass_spectrum"
 458                hdf_handle.attrs["analyzer"] = mass_spectrum.analyzer
 459                hdf_handle.attrs["instrument_label"] = mass_spectrum.instrument_label
 460                hdf_handle.attrs["sample_name"] = mass_spectrum.sample_name
 461
 462        list_results = self.list_dict_to_list(mass_spectrum, is_hdf5=True)
 463
 464        dict_ms_attrs = self.get_mass_spec_attrs(mass_spectrum)
 465
 466        setting_dicts = parameter_to_dict.get_dict_data_ms(mass_spectrum)
 467
 468        columns_labels = json.dumps(
 469            self.columns_label + self.get_all_used_atoms_in_order(mass_spectrum),
 470            sort_keys=False,
 471            indent=4,
 472            separators=(",", ": "),
 473        )
 474
 475        group_key = group_key
 476
 477        if mass_spectra_group is not None:
 478            hdf_handle = mass_spectra_group
 479
 480        if group_key not in hdf_handle.keys():
 481            scan_group = hdf_handle.create_group(group_key)
 482
 483            # If there is raw data (from profile data) save it
 484            if not mass_spectrum.is_centroid and export_raw:
 485                # "mz" is always the experimental (uncalibrated) profile; a
 486                # separate "mz_cal" row is added only when the spectrum is
 487                # actually calibrated. mz_cal_profile alone isn't enough to
 488                # tell: MzDomainCalibration.__init__ pre-populates it before
 489                # any calibration has run, so a never-calibrated spectrum
 490                # can still have a (stale/placeholder) mz_cal_profile.
 491                row_labels = ["mz", "abundance"]
 492                rows = [
 493                    np.asarray(mass_spectrum._mz_exp),
 494                    np.asarray(mass_spectrum.abundance_profile),
 495                ]
 496                mz_cal_profile = mass_spectrum.mz_cal_profile
 497                if mass_spectrum.is_calibrated and mz_cal_profile is not None:
 498                    row_labels.append("mz_cal")
 499                    rows.append(np.asarray(mz_cal_profile))
 500
 501                mz_abun_array = np.array(rows, dtype=profile_dtype)
 502
 503                raw_ms_dataset = scan_group.create_dataset(
 504                    "raw_ms",
 505                    data=mz_abun_array,
 506                    dtype=profile_dtype,
 507                    **hdf5_dataset_kwargs(mz_abun_array.shape, compression=compression),
 508                )
 509                raw_ms_dataset.attrs["row_labels"] = json.dumps(row_labels)
 510                # Recorded independently of whether an "mz_cal" row was
 511                # written, so a reader can trust this over row presence.
 512                raw_ms_dataset.attrs["is_calibrated"] = bool(
 513                    mass_spectrum.is_calibrated
 514                )
 515
 516                # Add metadata to the raw_ms dataset when it exists
 517                raw_ms_dataset.attrs["MassSpecAttrs"] = json.dumps(dict_ms_attrs)
 518
 519                if isinstance(mass_spectrum, MassSpecfromFreq):
 520                    raw_ms_dataset.attrs["TransientSetting"] = json.dumps(
 521                        setting_dicts.get("TransientSetting"),
 522                        sort_keys=False,
 523                        indent=4,
 524                        separators=(",", ": "),
 525                    )
 526            else:
 527                # For centroided data, store metadata directly on the scan group
 528                scan_group.attrs["MassSpecAttrs"] = json.dumps(dict_ms_attrs)
 529
 530                if isinstance(mass_spectrum, MassSpecfromFreq):
 531                    scan_group.attrs["TransientSetting"] = json.dumps(
 532                        setting_dicts.get("TransientSetting"),
 533                        sort_keys=False,
 534                        indent=4,
 535                        separators=(",", ": "),
 536                    )
 537
 538        else:
 539            scan_group = hdf_handle.get(group_key)
 540
 541        # Next processed-data index: one past the highest existing numeric
 542        # key, or 0 if there are none. "raw_ms" and "peak_profile_indices_*"
 543        # are not processed-data indices (get_time_index_to_pull also only
 544        # considers numeric keys, for the same reason). Using the max rather
 545        # than a count matters for a legacy file that already has "raw_ms"
 546        # and "1" (an old off-by-one: pre-fix code counted "raw_ms" as an
 547        # occupied slot) -- counting numeric keys would give "1" again and
 548        # collide with the existing one; max+1 gives "2".
 549        numeric_keys = [int(k) for k in scan_group.keys() if k.isdigit()]
 550        index_processed_data = str(max(numeric_keys) + 1) if numeric_keys else "0"
 551
 552        # Per-peak (left, apex, right) indices into the profile arrays above,
 553        # in mspeaks order, so a reloaded centroid-with-profile mass
 554        # spectrum doesn't need to re-derive peak boundaries from the peak
 555        # table's row position (which isn't a profile index). Written once
 556        # per peak-table version (same suffix as the processed-data index
 557        # below), since the profile itself (raw_ms) doesn't change across
 558        # reprocessing but the picked peaks do -- except raw_ms itself is
 559        # only ever written on the group's first creation (just above), so
 560        # if *this* mass_spectrum's profile is a different length (e.g. a
 561        # later reprocessing that re-picked over different raw data), these
 562        # indices would point into a profile that was never stored. Skip
 563        # writing them in that case; the reader falls back to a search.
 564        existing_raw_ms = scan_group.get("raw_ms")
 565        if (
 566            not mass_spectrum.is_centroid
 567            and export_raw
 568            and existing_raw_ms is not None
 569            and existing_raw_ms.shape[-1] == len(mass_spectrum.abundance_profile)
 570        ):
 571            peak_indices = np.array(
 572                [
 573                    (
 574                        mspeak.peak_left_index,
 575                        mspeak.peak_apex_index,
 576                        mspeak.peak_right_index,
 577                    )
 578                    for mspeak in mass_spectrum.mspeaks
 579                ],
 580                dtype=np.int32,
 581            ).reshape(-1, 3)
 582            scan_group.create_dataset(
 583                f"peak_profile_indices_{index_processed_data}",
 584                data=peak_indices,
 585                **hdf5_dataset_kwargs(peak_indices.shape, compression=compression),
 586            )
 587
 588        timenow = str(datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z"))
 589
 590        # Convert list_results to numpy array and optimize data types
 591        array_results = np.array(list_results)
 592
 593        # Apply data type optimization for numeric columns
 594        if array_results.dtype == np.float64:
 595            array_results = array_results.astype(np.float32)
 596        elif array_results.dtype == np.int64:
 597            array_results = array_results.astype(np.int32)
 598
 599        processed_dset = scan_group.create_dataset(
 600            index_processed_data,
 601            data=array_results,
 602            **hdf5_dataset_kwargs(array_results.shape, compression=compression),
 603        )
 604
 605        processed_dset.attrs["date_utc"] = timenow
 606
 607        processed_dset.attrs["ColumnsLabels"] = columns_labels
 608
 609        processed_dset.attrs["MoleculaSearchSetting"] = json.dumps(
 610            setting_dicts.get("MoleculaSearch"),
 611            sort_keys=False,
 612            indent=4,
 613            separators=(",", ": "),
 614        )
 615
 616        processed_dset.attrs["MassSpecPeakSetting"] = json.dumps(
 617            setting_dicts.get("MassSpecPeak"),
 618            sort_keys=False,
 619            indent=4,
 620            separators=(",", ": "),
 621        )
 622
 623        processed_dset.attrs["MassSpectrumSetting"] = json.dumps(
 624            setting_dicts.get("MassSpectrum"),
 625            sort_keys=False,
 626            indent=4,
 627            separators=(",", ": "),
 628        )
 629
 630    def to_hdf(self, compression=DEFAULT_HDF5_COMPRESSION, profile_dtype="f4"):
 631        """Exports the mass spectrum data to an HDF5 file.
 632
 633        Parameters
 634        ----------
 635        compression : int, str or None, optional
 636            Compression for the written datasets: a gzip level 0-9,
 637            ``"lzf"``, or ``None``/``"none"``. Default is 4. ``"lzf"`` is
 638            h5py's own filter, not a standard HDF5 one -- it needs h5py (or
 639            a tool with the lzf plugin) to read the result back.
 640        profile_dtype : str, optional
 641            dtype used for the stored profile m/z/abundance arrays
 642            (``"f4"`` or ``"f8"``). Default is ``"f4"``.
 643        """
 644
 645        with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle:
 646            self.add_mass_spectrum_to_hdf5(
 647                hdf_handle,
 648                self.mass_spectrum,
 649                str(self.mass_spectrum.scan_number),
 650                compression=compression,
 651                profile_dtype=profile_dtype,
 652            )
 653
 654    def parameters_to_toml(self):
 655        """Converts the mass spectrum parameters to a TOML string.
 656
 657        Returns
 658        -------
 659        str
 660            The TOML string of the mass spectrum parameters.
 661        """
 662
 663        dict_setting = parameter_to_dict.get_dict_data_ms(self.mass_spectrum)
 664
 665        dict_setting["MassSpecAttrs"] = self.get_mass_spec_attrs(self.mass_spectrum)
 666        dict_setting["analyzer"] = self.mass_spectrum.analyzer
 667        dict_setting["instrument_label"] = self.mass_spectrum.instrument_label
 668        dict_setting["sample_name"] = self.mass_spectrum.sample_name
 669
 670        output = _dumps_toml(dict_setting)
 671
 672        return output
 673
 674    def parameters_to_json(self):
 675        """Converts the mass spectrum parameters to a JSON string.
 676
 677        Returns
 678        -------
 679        str
 680            The JSON string of the mass spectrum parameters.
 681        """
 682
 683        dict_setting = parameter_to_dict.get_dict_data_ms(self.mass_spectrum)
 684
 685        dict_setting["MassSpecAttrs"] = self.get_mass_spec_attrs(self.mass_spectrum)
 686        dict_setting["analyzer"] = self.mass_spectrum.analyzer
 687        dict_setting["instrument_label"] = self.mass_spectrum.instrument_label
 688        dict_setting["sample_name"] = self.mass_spectrum.sample_name
 689
 690        output = json.dumps(dict_setting)
 691
 692        return output
 693
 694    def get_mass_spec_attrs(self, mass_spectrum):
 695        """Returns the mass spectrum attributes as a dictionary.
 696
 697        Parameters
 698        ----------
 699        mass_spectrum : MassSpectrum
 700            The mass spectrum to export.
 701
 702        Returns
 703        -------
 704        dict
 705            The mass spectrum attributes.
 706        """
 707
 708        dict_ms_attrs = {}
 709        dict_ms_attrs["polarity"] = mass_spectrum.polarity
 710        dict_ms_attrs["rt"] = mass_spectrum.retention_time
 711        dict_ms_attrs["tic"] = mass_spectrum.tic
 712        dict_ms_attrs["mobility_scan"] = mass_spectrum.mobility_scan
 713        dict_ms_attrs["mobility_rt"] = mass_spectrum.mobility_rt
 714        dict_ms_attrs["Aterm"] = mass_spectrum.Aterm
 715        dict_ms_attrs["Bterm"] = mass_spectrum.Bterm
 716        dict_ms_attrs["Cterm"] = mass_spectrum.Cterm
 717        dict_ms_attrs["baseline_noise"] = mass_spectrum.baseline_noise
 718        dict_ms_attrs["baseline_noise_std"] = mass_spectrum.baseline_noise_std
 719
 720        return dict_ms_attrs
 721
 722    def get_all_used_atoms_in_order(self, mass_spectrum):
 723        """Returns the list of assigned atoms in the order specified by Atoms.atoms_order list.
 724
 725        Parameters
 726        ----------
 727        mass_spectrum : MassSpectrum
 728            The mass spectrum to export.
 729
 730        Returns
 731        -------
 732        list
 733            The list of assigned atoms in the order specified by Atoms.atoms_order list.
 734        """
 735
 736        atoms_in_order = Atoms.atoms_order
 737        all_used_atoms = set()
 738        if mass_spectrum:
 739            for ms_peak in mass_spectrum:
 740                if ms_peak:
 741                    for m_formula in ms_peak:
 742                        for atom in m_formula.atoms:
 743                            all_used_atoms.add(atom)
 744
 745        def sort_method(atom):
 746            return [atoms_in_order.index(atom)]
 747
 748        return sorted(all_used_atoms, key=sort_method)
 749
 750    def list_dict_to_list(self, mass_spectrum, is_hdf5=False):
 751        """Returns the mass spectrum data as a list of dictionaries.
 752
 753        Parameters
 754        ----------
 755        mass_spectrum : MassSpectrum
 756            The mass spectrum to export.
 757        is_hdf5 : bool, optional
 758            Whether the mass spectrum is being exported to an HDF5 file. Defaults to False.
 759
 760        Returns
 761        -------
 762        list
 763            The mass spectrum data as a list of dictionaries.
 764        """
 765
 766        column_labels = self.columns_label + self.get_all_used_atoms_in_order(
 767            mass_spectrum
 768        )
 769
 770        dict_list = self.get_list_dict_data(mass_spectrum, is_hdf5=is_hdf5)
 771
 772        all_lines = []
 773        for dict_res in dict_list:
 774            result_line = [NaN] * len(column_labels)
 775
 776            for label, value in dict_res.items():
 777                label_index = column_labels.index(label)
 778                result_line[label_index] = value
 779
 780            all_lines.append(result_line)
 781
 782        return all_lines
 783
 784    def get_list_dict_data(
 785        self,
 786        mass_spectrum,
 787        include_no_match=True,
 788        include_isotopologues=True,
 789        isotopologue_inline=True,
 790        no_match_inline=False,
 791        is_hdf5=False,
 792        additional_columns=None,
 793    ):
 794        """Returns the mass spectrum data as a list of dictionaries.
 795
 796        Parameters
 797        ----------
 798        mass_spectrum : MassSpectrum
 799            The mass spectrum to export.
 800        include_no_match : bool, optional
 801            Whether to include unassigned (no match) data. Defaults to True.
 802        include_isotopologues : bool, optional
 803            Whether to include isotopologues. Defaults to True.
 804        isotopologue_inline : bool, optional
 805            Whether to include isotopologues inline. Defaults to True.
 806        no_match_inline : bool, optional
 807            Whether to include unassigned (no match) data inline. Defaults to False.
 808        is_hdf5 : bool, optional
 809            Whether the mass spectrum is being exported to an HDF5 file. Defaults to False.
 810
 811        Returns
 812        -------
 813        list
 814            The mass spectrum data as a list of dictionaries.
 815        """
 816
 817        dict_data_list = []
 818
 819        if is_hdf5:
 820            encode = ".encode('utf-8')"
 821        else:
 822            encode = ""
 823
 824        def add_no_match_dict_data(index, ms_peak):
 825            """
 826            Export dictionary of mspeak info for unassigned (no match) data
 827            """
 828            dict_result = {
 829                "Index": index,
 830                "m/z": ms_peak._mz_exp,
 831                "Calibrated m/z": ms_peak.mz_exp,
 832                "Peak Height": ms_peak.abundance,
 833                "Peak Area": ms_peak.area,
 834                "Resolving Power": ms_peak.resolving_power,
 835                "S/N": ms_peak.signal_to_noise,
 836                # Unassigned: polarity only (±1); no formula charge exists yet
 837                "Ion Charge": ms_peak.polarity,
 838                "Heteroatom Class": eval("Labels.unassigned{}".format(encode)),
 839            }
 840
 841            dict_data_list.append(dict_result)
 842
 843        def add_match_dict_data(index, ms_peak, mformula, additional_columns=None):
 844            """
 845            Export dictionary of mspeak info for assigned (match) data
 846            """
 847            formula_dict = mformula.to_dict()
 848
 849            dict_result = {
 850                "Index": index,
 851                "m/z": ms_peak._mz_exp,
 852                "Calibrated m/z": ms_peak.mz_exp,
 853                "Calculated m/z": mformula.mz_calc,
 854                "Peak Height": ms_peak.abundance,
 855                "Peak Area": ms_peak.area,
 856                "Resolving Power": ms_peak.resolving_power,
 857                "S/N": ms_peak.signal_to_noise,
 858                # Assigned: formula charge state (may be multi-charge); not peak polarity
 859                "Ion Charge": mformula.ion_charge,
 860                "m/z Error (ppm)": mformula.mz_error,
 861                "Confidence Score": mformula.confidence_score,
 862                "Isotopologue Similarity": mformula.isotopologue_similarity,
 863                "m/z Error Score": mformula.average_mz_error_score,
 864                "DBE": mformula.dbe,
 865                "Heteroatom Class": eval("mformula.class_label{}".format(encode)),
 866                "H/C": mformula.H_C,
 867                "O/C": mformula.O_C,
 868                "Ion Type": eval("mformula.ion_type.lower(){}".format(encode)),
 869                "Is Isotopologue": int(mformula.is_isotopologue),
 870                "Molecular Formula": eval("mformula.string{}".format(encode)),
 871            }
 872            if additional_columns is not None:
 873                possible_dict = {
 874                    "Aromaticity Index": mformula.A_I,
 875                    "NOSC": mformula.nosc,
 876                    "Aromaticity Index (modified)": mformula.A_I_mod,
 877                }
 878                for column in additional_columns:
 879                    dict_result[column] = possible_dict.get(column)
 880
 881            if mformula.adduct_atom:
 882                dict_result["Adduct"] = eval("mformula.adduct_atom{}".format(encode))
 883
 884            if mformula.is_isotopologue:
 885                dict_result["Mono Isotopic Index"] = mformula.mspeak_index_mono_isotopic
 886
 887            if self.atoms_order_list is None:
 888                atoms_order_list = self.get_all_used_atoms_in_order(mass_spectrum)
 889            else:
 890                atoms_order_list = self.atoms_order_list
 891
 892            for atom in atoms_order_list:
 893                if atom in formula_dict.keys():
 894                    dict_result[atom] = formula_dict.get(atom)
 895
 896            dict_data_list.append(dict_result)
 897
 898        score_methods = mass_spectrum.molecular_search_settings.score_methods
 899        selected_score_method = (
 900            mass_spectrum.molecular_search_settings.output_score_method
 901        )
 902
 903        if selected_score_method in score_methods:
 904            # temp set score method as the one chosen in the output
 905            current_method = mass_spectrum.molecular_search_settings.score_method
 906            mass_spectrum.molecular_search_settings.score_method = selected_score_method
 907
 908            for index, ms_peak in enumerate(mass_spectrum):
 909                # print(ms_peak.mz_exp)
 910
 911                if ms_peak:
 912                    m_formula = ms_peak.best_molecular_formula_candidate
 913
 914                    if m_formula:
 915                        if not m_formula.is_isotopologue:
 916                            add_match_dict_data(
 917                                index,
 918                                ms_peak,
 919                                m_formula,
 920                                additional_columns=additional_columns,
 921                            )
 922
 923                            for (
 924                                iso_mspeak_index,
 925                                iso_mf_formula,
 926                            ) in m_formula.mspeak_mf_isotopologues_indexes:
 927                                iso_ms_peak = mass_spectrum[iso_mspeak_index]
 928                                add_match_dict_data(
 929                                    iso_mspeak_index,
 930                                    iso_ms_peak,
 931                                    iso_mf_formula,
 932                                    additional_columns=additional_columns,
 933                                )
 934                else:
 935                    if include_no_match and no_match_inline:
 936                        add_no_match_dict_data(index, ms_peak)
 937
 938            if include_no_match and not no_match_inline:
 939                for index, ms_peak in enumerate(mass_spectrum):
 940                    if not ms_peak:
 941                        add_no_match_dict_data(index, ms_peak)
 942            # reset score method as the one chosen in the output
 943            mass_spectrum.molecular_search_settings.score_method = current_method
 944
 945        else:
 946            for index, ms_peak in enumerate(mass_spectrum):
 947                # check if there is a molecular formula candidate for the msPeak
 948
 949                if ms_peak:
 950                    # m_formula = ms_peak.molecular_formula_lowest_error
 951                    for m_formula in ms_peak:
 952                        if mass_spectrum.molecular_search_settings.output_min_score > 0:
 953                            if (
 954                                m_formula.confidence_score
 955                                >= mass_spectrum.molecular_search_settings.output_min_score
 956                            ):
 957                                if m_formula.is_isotopologue:  # isotopologues inline
 958                                    if include_isotopologues and isotopologue_inline:
 959                                        add_match_dict_data(
 960                                            index,
 961                                            ms_peak,
 962                                            m_formula,
 963                                            additional_columns=additional_columns,
 964                                        )
 965                                else:
 966                                    add_match_dict_data(
 967                                        index,
 968                                        ms_peak,
 969                                        m_formula,
 970                                        additional_columns=additional_columns,
 971                                    )  # add monoisotopic peak
 972
 973                            # cutoff because of low score
 974                            else:
 975                                add_no_match_dict_data(index, ms_peak)
 976
 977                        else:
 978                            if m_formula.is_isotopologue:  # isotopologues inline
 979                                if include_isotopologues and isotopologue_inline:
 980                                    add_match_dict_data(
 981                                        index,
 982                                        ms_peak,
 983                                        m_formula,
 984                                        additional_columns=additional_columns,
 985                                    )
 986                            else:
 987                                add_match_dict_data(
 988                                    index,
 989                                    ms_peak,
 990                                    m_formula,
 991                                    additional_columns=additional_columns,
 992                                )  # add monoisotopic peak
 993                else:
 994                    # include not_match
 995                    if include_no_match and no_match_inline:
 996                        add_no_match_dict_data(index, ms_peak)
 997
 998            if include_isotopologues and not isotopologue_inline:
 999                for index, ms_peak in enumerate(mass_spectrum):
1000                    for m_formula in ms_peak:
1001                        if m_formula.is_isotopologue:
1002                            if (
1003                                m_formula.confidence_score
1004                                >= mass_spectrum.molecular_search_settings.output_min_score
1005                            ):
1006                                add_match_dict_data(
1007                                    index,
1008                                    ms_peak,
1009                                    m_formula,
1010                                    additional_columns=additional_columns,
1011                                )
1012
1013            if include_no_match and not no_match_inline:
1014                for index, ms_peak in enumerate(mass_spectrum):
1015                    if not ms_peak:
1016                        add_no_match_dict_data(index, ms_peak)
1017
1018        # remove duplicated add_match data possibly introduced on the output_score_filter step
1019        res = []
1020        [res.append(x) for x in dict_data_list if x not in res]
1021
1022        return res
def hdf5_dataset_kwargs(shape, compression=4, vlen_str=False):
26def hdf5_dataset_kwargs(shape, compression=DEFAULT_HDF5_COMPRESSION, vlen_str=False):
27    """Build ``h5py.Group.create_dataset`` filter kwargs for a compression choice.
28
29    Centralizes the HDF5 writers' compression choice (one helper used by every
30    ``create_dataset`` call on the mass spectrum / LC-MS export path) and the
31    filter exceptions h5py enforces: it refuses chunk/filter options on scalar
32    (``shape == ()``) or zero-length datasets, and the fletcher32 checksum
33    filter gains nothing (and h5py can reject it) on variable-length string
34    datasets.
35
36    Parameters
37    ----------
38    shape : tuple
39        Shape of the data about to be written.
40    compression : int, str or None, optional
41        A gzip level 0-9, ``"lzf"``, or ``None``/``"none"`` for no
42        compression. Default is 4. ``"lzf"`` is h5py's own filter, not part
43        of core HDF5 -- a file written with it needs h5py (or another tool
44        with the lzf plugin available) to read the compressed datasets
45        back; gzip is the portable choice across other HDF5 tools.
46    vlen_str : bool, optional
47        True when the dataset's dtype is a variable-length string
48        (``h5py.string_dtype()``); skips the shuffle/fletcher32 filters that
49        don't apply to vlen data. Default is False.
50
51    Returns
52    -------
53    dict
54        Keyword arguments to splat into ``create_dataset`` (besides ``data``
55        and ``dtype``).
56    """
57    if not shape or 0 in shape:
58        return {}
59
60    kwargs = {"chunks": True}
61
62    if compression not in (None, "none"):
63        if compression == "lzf":
64            kwargs["compression"] = "lzf"
65        else:
66            kwargs["compression"] = "gzip"
67            kwargs["compression_opts"] = int(compression)
68
69    if not vlen_str:
70        kwargs["shuffle"] = True
71        kwargs["fletcher32"] = True
72
73    return kwargs

Build h5py.Group.create_dataset filter kwargs for a compression choice.

Centralizes the HDF5 writers' compression choice (one helper used by every create_dataset call on the mass spectrum / LC-MS export path) and the filter exceptions h5py enforces: it refuses chunk/filter options on scalar (shape == ()) or zero-length datasets, and the fletcher32 checksum filter gains nothing (and h5py can reject it) on variable-length string datasets.

Parameters
  • shape (tuple): Shape of the data about to be written.
  • compression (int, str or None, optional): A gzip level 0-9, "lzf", or None/"none" for no compression. Default is 4. "lzf" is h5py's own filter, not part of core HDF5 -- a file written with it needs h5py (or another tool with the lzf plugin available) to read the compressed datasets back; gzip is the portable choice across other HDF5 tools.
  • vlen_str (bool, optional): True when the dataset's dtype is a variable-length string (h5py.string_dtype()); skips the shuffle/fletcher32 filters that don't apply to vlen data. Default is False.
Returns
  • dict: Keyword arguments to splat into create_dataset (besides data and dtype).
class HighResMassSpecExport(threading.Thread):
  76class HighResMassSpecExport(Thread):
  77    """A class for exporting high-resolution mass spectra.
  78
  79    Parameters
  80    ----------
  81    out_file_path : str
  82        The output file path.
  83    mass_spectrum : MassSpectrum
  84        The mass spectrum to export.
  85    output_type : str, optional
  86        The type of output file. Defaults to 'excel'. Can be 'excel', 'csv', 'pandas', 'parquet' or 'hdf5'.
  87
  88    Attributes
  89    ----------
  90    output_file : Path
  91        The output file path.
  92    output_type : str
  93        The type of output file.
  94    mass_spectrum : MassSpectrum
  95        The mass spectrum to export.
  96    atoms_order_list : list
  97        The list of assigned atoms in the order specified by Atoms.atoms_order list.
  98    columns_label : list
  99        The column labels in order.
 100
 101    Methods
 102    -------
 103    * save().
 104        Save the mass spectrum data to the output file.
 105    * run().
 106        Run the export process.
 107    * get_pandas_df().
 108        Returns the mass spectrum data as a pandas DataFrame.
 109    * write_settings(output_path, mass_spectrum).
 110        Writes the settings of the mass spectrum to a JSON file.
 111    * to_pandas(write_metadata=True).
 112        Exports the mass spectrum data to a pandas DataFrame and saves it as a pickle file.
 113    * to_excel(write_metadata=True).
 114        Exports the mass spectrum data to an Excel file.
 115    * to_csv(write_metadata=True).
 116        Exports the mass spectrum data to a CSV file.
 117    * to_parquet(write_metadata=True).
 118        Exports the mass spectrum data to a Parquet file.
 119    * to_json().
 120        Exports the mass spectrum data to a JSON string.
 121    * to_hdf().
 122        Exports the mass spectrum data to an HDF5 file.
 123    * parameters_to_toml().
 124        Converts the mass spectrum parameters to a TOML string.
 125    * parameters_to_json().
 126        Converts the mass spectrum parameters to a JSON string.
 127    * get_mass_spec_attrs(mass_spectrum).
 128        Returns the mass spectrum attributes as a dictionary.
 129    * get_all_used_atoms_in_order(mass_spectrum).
 130        Returns the list of assigned atoms in the order specified by Atoms.atoms_order list.
 131    * list_dict_to_list(mass_spectrum, is_hdf5=False).
 132        Returns the mass spectrum data as a list of dictionaries.
 133    * get_list_dict_data(mass_spectrum, include_no_match=True, include_isotopologues=True, isotopologue_inline=True, no_match_inline=False, is_hdf5=False).
 134        Returns the mass spectrum data as a list of dictionaries.
 135
 136    """
 137
 138    def __init__(self, out_file_path, mass_spectrum, output_type="excel"):
 139        Thread.__init__(self)
 140
 141        self.output_file = Path(out_file_path)
 142
 143        # 'excel', 'csv' or 'pandas'
 144        self.output_type = output_type
 145
 146        self.mass_spectrum = mass_spectrum
 147
 148        # collect all assigned atoms and order them accordingly to the Atoms.atoms_order list
 149        self.atoms_order_list = self.get_all_used_atoms_in_order(self.mass_spectrum)
 150
 151        self._init_columns()
 152
 153    def _init_columns(self):
 154        """Initialize the columns for the mass spectrum output."""
 155        # column labels in order
 156        self.columns_label = [
 157            "Index",
 158            "m/z",
 159            "Calibrated m/z",
 160            "Calculated m/z",
 161            "Peak Height",
 162            "Peak Area",
 163            "Resolving Power",
 164            "S/N",
 165            "Ion Charge",
 166            "m/z Error (ppm)",
 167            "m/z Error Score",
 168            "Isotopologue Similarity",
 169            "Confidence Score",
 170            "DBE",
 171            "O/C",
 172            "H/C",
 173            "Heteroatom Class",
 174            "Ion Type",
 175            "Adduct",
 176            "Is Isotopologue",
 177            "Mono Isotopic Index",
 178            "Molecular Formula",
 179        ]
 180
 181    @property
 182    def output_type(self):
 183        """Returns the output type of the mass spectrum."""
 184        return self._output_type
 185
 186    @output_type.setter
 187    def output_type(self, output_type):
 188        output_types = ["excel", "csv", "pandas", "parquet", "hdf5"]
 189        if output_type in output_types:
 190            self._output_type = output_type
 191        else:
 192            raise TypeError(
 193                'Supported types are "excel", "csv", "pandas", "parquet" or "hdf5", %s entered'
 194                % output_type
 195            )
 196
 197    def save(self):
 198        """Save the mass spectrum data to the output file.
 199
 200        Raises
 201        ------
 202        ValueError
 203            If the output type is not supported.
 204        """
 205
 206        if self.output_type == "excel":
 207            self.to_excel()
 208        elif self.output_type == "csv":
 209            self.to_csv()
 210        elif self.output_type == "pandas":
 211            self.to_pandas()
 212        elif self.output_type == "parquet":
 213            self.to_parquet()
 214        elif self.output_type == "hdf5":
 215            self.to_hdf()
 216        else:
 217            raise ValueError(
 218                "Unkown output type: %s; it can be 'excel', 'csv', 'pandas', 'parquet' or 'hdf5'"
 219                % self.output_type
 220            )
 221
 222    def run(self):
 223        """Run the export process.
 224
 225        This method is called when the thread starts.
 226        It calls the save method to perform the export."""
 227        self.save()
 228
 229    def get_pandas_df(self, additional_columns=None):
 230        """Returns the mass spectrum data as a pandas DataFrame.
 231
 232        Parameters
 233        ----------
 234        additional_columns : list, optional
 235            Additional columns to include in the DataFrame. Defaults to None.
 236            Suitable additional columns are: 'Aromaticity Index', 'NOSC', 'Aromaticity Index (modified)'.
 237
 238        Returns
 239        -------
 240        DataFrame
 241            The mass spectrum data as a pandas DataFrame.
 242        """
 243        if additional_columns is not None:
 244            possible_additional_columns = [
 245                "Aromaticity Index",
 246                "NOSC",
 247                "Aromaticity Index (modified)",
 248            ]
 249            if additional_columns:
 250                for column in additional_columns:
 251                    if column not in possible_additional_columns:
 252                        raise ValueError("Invalid additional column: %s" % column)
 253            columns = (
 254                self.columns_label
 255                + additional_columns
 256                + self.get_all_used_atoms_in_order(self.mass_spectrum)
 257            )
 258        else:
 259            columns = self.columns_label + self.get_all_used_atoms_in_order(
 260                self.mass_spectrum
 261            )
 262        dict_data_list = self.get_list_dict_data(
 263            self.mass_spectrum, additional_columns=additional_columns
 264        )
 265        df = DataFrame(dict_data_list, columns=columns)
 266        df.attrs["name"] = self.output_file
 267        return df
 268
 269    def write_settings(self, output_path, mass_spectrum):
 270        """Writes the settings of the mass spectrum to a JSON file.
 271
 272        Parameters
 273        ----------
 274        output_path : str
 275            The output file path.
 276        mass_spectrum : MassSpectrum
 277            The mass spectrum to export.
 278        """
 279
 280        import json
 281
 282        dict_setting = parameter_to_dict.get_dict_data_ms(mass_spectrum)
 283
 284        dict_setting["MassSpecAttrs"] = self.get_mass_spec_attrs(mass_spectrum)
 285        dict_setting["analyzer"] = mass_spectrum.analyzer
 286        dict_setting["instrument_label"] = mass_spectrum.instrument_label
 287        dict_setting["sample_name"] = mass_spectrum.sample_name
 288
 289        with open(
 290            output_path.with_suffix(".json"),
 291            "w",
 292            encoding="utf8",
 293        ) as outfile:
 294            output = json.dumps(
 295                dict_setting, sort_keys=True, indent=4, separators=(",", ": ")
 296            )
 297            outfile.write(output)
 298
 299    def to_pandas(self, write_metadata=True):
 300        """Exports the mass spectrum data to a pandas DataFrame and saves it as a pickle file.
 301
 302        Parameters
 303        ----------
 304        write_metadata : bool, optional
 305            Whether to write the metadata to a JSON file. Defaults to True.
 306        """
 307
 308        columns = self.columns_label + self.get_all_used_atoms_in_order(
 309            self.mass_spectrum
 310        )
 311
 312        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
 313
 314        df = DataFrame(dict_data_list, columns=columns)
 315
 316        df.to_pickle(self.output_file.with_suffix(".pkl"))
 317
 318        if write_metadata:
 319            self.write_settings(self.output_file, self.mass_spectrum)
 320
 321    def to_excel(self, write_metadata=True):
 322        """Exports the mass spectrum data to an Excel file.
 323
 324        Parameters
 325        ----------
 326        write_metadata : bool, optional
 327            Whether to write the metadata to a JSON file. Defaults to True.
 328        """
 329
 330        columns = self.columns_label + self.get_all_used_atoms_in_order(
 331            self.mass_spectrum
 332        )
 333
 334        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
 335
 336        df = DataFrame(dict_data_list, columns=columns)
 337
 338        df.to_excel(self.output_file.with_suffix(".xlsx"))
 339
 340        if write_metadata:
 341            self.write_settings(self.output_file, self.mass_spectrum)
 342
 343    def to_csv(self, write_metadata=True):
 344        """Exports the mass spectrum data to a CSV file.
 345
 346        Parameters
 347        ----------
 348        write_metadata : bool, optional
 349            Whether to write the metadata to a JSON file. Defaults to True.
 350        """
 351
 352        columns = self.columns_label + self.get_all_used_atoms_in_order(
 353            self.mass_spectrum
 354        )
 355
 356        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
 357
 358        import csv
 359
 360        try:
 361            with open(self.output_file.with_suffix(".csv"), "w", newline="") as csvfile:
 362                writer = csv.DictWriter(csvfile, fieldnames=columns)
 363                writer.writeheader()
 364                for data in dict_data_list:
 365                    writer.writerow(data)
 366            if write_metadata:
 367                self.write_settings(self.output_file, self.mass_spectrum)
 368
 369        except IOError as ioerror:
 370            print(ioerror)
 371
 372    def to_parquet(self, write_metadata=True):
 373        """Exports the mass spectrum data to a Parquet file.
 374
 375        Parameters
 376        ----------
 377        write_metadata : bool, optional
 378            Whether to write the metadata to a JSON file. Defaults to True.
 379
 380        Notes
 381        -----
 382        Requires ``pyarrow`` (or ``fastparquet``) for pandas Parquet I/O.
 383        """
 384
 385        columns = self.columns_label + self.get_all_used_atoms_in_order(
 386            self.mass_spectrum
 387        )
 388
 389        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
 390
 391        df = DataFrame(dict_data_list, columns=columns)
 392
 393        df.to_parquet(self.output_file.with_suffix(".parquet"), index=False)
 394
 395        if write_metadata:
 396            self.write_settings(self.output_file, self.mass_spectrum)
 397
 398    def to_json(self):
 399        """Exports the mass spectrum data to a JSON string."""
 400
 401        columns = self.columns_label + self.get_all_used_atoms_in_order(
 402            self.mass_spectrum
 403        )
 404
 405        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
 406
 407        df = DataFrame(dict_data_list, columns=columns)
 408
 409        # for key, values in dict_data.items():
 410        #    if not values: dict_data[key] = NaN
 411
 412        # output = json.dumps(dict_data, sort_keys=True, indent=4, separators=(',', ': '))
 413        return df.to_json(orient="records")
 414
 415    def add_mass_spectrum_to_hdf5(
 416        self,
 417        hdf_handle,
 418        mass_spectrum,
 419        group_key,
 420        mass_spectra_group=None,
 421        export_raw=True,
 422        compression=DEFAULT_HDF5_COMPRESSION,
 423        profile_dtype="f4",
 424    ):
 425        """Adds the mass spectrum data to an HDF5 file.
 426
 427        Parameters
 428        ----------
 429        hdf_handle : h5py.File
 430            The HDF5 file handle.
 431        mass_spectrum : MassSpectrum
 432            The mass spectrum to add to the HDF5 file.
 433        group_key : str
 434            The group key (where to add the mass spectrum data within the HDF5 file).
 435        mass_spectra_group : h5py.Group, optional
 436            The mass spectra group. Defaults to None (no group, mass spectrum is added to the root).
 437        export_raw : bool, optional
 438            Whether to export the raw data. Defaults to True.
 439            If False, only the processed data (peaks) is exported (essentially centroided data).
 440        compression : int, str or None, optional
 441            Compression for the written datasets: a gzip level 0-9, ``"lzf"``,
 442            or ``None``/``"none"``. Default is 4. ``"lzf"`` is h5py's own
 443            filter, not a standard HDF5 one -- it needs h5py (or a tool
 444            with the lzf plugin) to read the result back.
 445        profile_dtype : str, optional
 446            dtype used for the stored profile m/z/abundance arrays
 447            (``"f4"`` or ``"f8"``). Default is ``"f4"``.
 448        """
 449        if mass_spectra_group is None:
 450            # Check if the file has the necessary attributes and add them if not
 451            # This assumes that if there is a mass_spectra_group, these attributes were already added to the file
 452            if not hdf_handle.attrs.get("date_utc"):
 453                timenow = str(
 454                    datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z")
 455                )
 456                hdf_handle.attrs["date_utc"] = timenow
 457                hdf_handle.attrs["file_name"] = mass_spectrum.filename.name
 458                hdf_handle.attrs["data_structure"] = "mass_spectrum"
 459                hdf_handle.attrs["analyzer"] = mass_spectrum.analyzer
 460                hdf_handle.attrs["instrument_label"] = mass_spectrum.instrument_label
 461                hdf_handle.attrs["sample_name"] = mass_spectrum.sample_name
 462
 463        list_results = self.list_dict_to_list(mass_spectrum, is_hdf5=True)
 464
 465        dict_ms_attrs = self.get_mass_spec_attrs(mass_spectrum)
 466
 467        setting_dicts = parameter_to_dict.get_dict_data_ms(mass_spectrum)
 468
 469        columns_labels = json.dumps(
 470            self.columns_label + self.get_all_used_atoms_in_order(mass_spectrum),
 471            sort_keys=False,
 472            indent=4,
 473            separators=(",", ": "),
 474        )
 475
 476        group_key = group_key
 477
 478        if mass_spectra_group is not None:
 479            hdf_handle = mass_spectra_group
 480
 481        if group_key not in hdf_handle.keys():
 482            scan_group = hdf_handle.create_group(group_key)
 483
 484            # If there is raw data (from profile data) save it
 485            if not mass_spectrum.is_centroid and export_raw:
 486                # "mz" is always the experimental (uncalibrated) profile; a
 487                # separate "mz_cal" row is added only when the spectrum is
 488                # actually calibrated. mz_cal_profile alone isn't enough to
 489                # tell: MzDomainCalibration.__init__ pre-populates it before
 490                # any calibration has run, so a never-calibrated spectrum
 491                # can still have a (stale/placeholder) mz_cal_profile.
 492                row_labels = ["mz", "abundance"]
 493                rows = [
 494                    np.asarray(mass_spectrum._mz_exp),
 495                    np.asarray(mass_spectrum.abundance_profile),
 496                ]
 497                mz_cal_profile = mass_spectrum.mz_cal_profile
 498                if mass_spectrum.is_calibrated and mz_cal_profile is not None:
 499                    row_labels.append("mz_cal")
 500                    rows.append(np.asarray(mz_cal_profile))
 501
 502                mz_abun_array = np.array(rows, dtype=profile_dtype)
 503
 504                raw_ms_dataset = scan_group.create_dataset(
 505                    "raw_ms",
 506                    data=mz_abun_array,
 507                    dtype=profile_dtype,
 508                    **hdf5_dataset_kwargs(mz_abun_array.shape, compression=compression),
 509                )
 510                raw_ms_dataset.attrs["row_labels"] = json.dumps(row_labels)
 511                # Recorded independently of whether an "mz_cal" row was
 512                # written, so a reader can trust this over row presence.
 513                raw_ms_dataset.attrs["is_calibrated"] = bool(
 514                    mass_spectrum.is_calibrated
 515                )
 516
 517                # Add metadata to the raw_ms dataset when it exists
 518                raw_ms_dataset.attrs["MassSpecAttrs"] = json.dumps(dict_ms_attrs)
 519
 520                if isinstance(mass_spectrum, MassSpecfromFreq):
 521                    raw_ms_dataset.attrs["TransientSetting"] = json.dumps(
 522                        setting_dicts.get("TransientSetting"),
 523                        sort_keys=False,
 524                        indent=4,
 525                        separators=(",", ": "),
 526                    )
 527            else:
 528                # For centroided data, store metadata directly on the scan group
 529                scan_group.attrs["MassSpecAttrs"] = json.dumps(dict_ms_attrs)
 530
 531                if isinstance(mass_spectrum, MassSpecfromFreq):
 532                    scan_group.attrs["TransientSetting"] = json.dumps(
 533                        setting_dicts.get("TransientSetting"),
 534                        sort_keys=False,
 535                        indent=4,
 536                        separators=(",", ": "),
 537                    )
 538
 539        else:
 540            scan_group = hdf_handle.get(group_key)
 541
 542        # Next processed-data index: one past the highest existing numeric
 543        # key, or 0 if there are none. "raw_ms" and "peak_profile_indices_*"
 544        # are not processed-data indices (get_time_index_to_pull also only
 545        # considers numeric keys, for the same reason). Using the max rather
 546        # than a count matters for a legacy file that already has "raw_ms"
 547        # and "1" (an old off-by-one: pre-fix code counted "raw_ms" as an
 548        # occupied slot) -- counting numeric keys would give "1" again and
 549        # collide with the existing one; max+1 gives "2".
 550        numeric_keys = [int(k) for k in scan_group.keys() if k.isdigit()]
 551        index_processed_data = str(max(numeric_keys) + 1) if numeric_keys else "0"
 552
 553        # Per-peak (left, apex, right) indices into the profile arrays above,
 554        # in mspeaks order, so a reloaded centroid-with-profile mass
 555        # spectrum doesn't need to re-derive peak boundaries from the peak
 556        # table's row position (which isn't a profile index). Written once
 557        # per peak-table version (same suffix as the processed-data index
 558        # below), since the profile itself (raw_ms) doesn't change across
 559        # reprocessing but the picked peaks do -- except raw_ms itself is
 560        # only ever written on the group's first creation (just above), so
 561        # if *this* mass_spectrum's profile is a different length (e.g. a
 562        # later reprocessing that re-picked over different raw data), these
 563        # indices would point into a profile that was never stored. Skip
 564        # writing them in that case; the reader falls back to a search.
 565        existing_raw_ms = scan_group.get("raw_ms")
 566        if (
 567            not mass_spectrum.is_centroid
 568            and export_raw
 569            and existing_raw_ms is not None
 570            and existing_raw_ms.shape[-1] == len(mass_spectrum.abundance_profile)
 571        ):
 572            peak_indices = np.array(
 573                [
 574                    (
 575                        mspeak.peak_left_index,
 576                        mspeak.peak_apex_index,
 577                        mspeak.peak_right_index,
 578                    )
 579                    for mspeak in mass_spectrum.mspeaks
 580                ],
 581                dtype=np.int32,
 582            ).reshape(-1, 3)
 583            scan_group.create_dataset(
 584                f"peak_profile_indices_{index_processed_data}",
 585                data=peak_indices,
 586                **hdf5_dataset_kwargs(peak_indices.shape, compression=compression),
 587            )
 588
 589        timenow = str(datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z"))
 590
 591        # Convert list_results to numpy array and optimize data types
 592        array_results = np.array(list_results)
 593
 594        # Apply data type optimization for numeric columns
 595        if array_results.dtype == np.float64:
 596            array_results = array_results.astype(np.float32)
 597        elif array_results.dtype == np.int64:
 598            array_results = array_results.astype(np.int32)
 599
 600        processed_dset = scan_group.create_dataset(
 601            index_processed_data,
 602            data=array_results,
 603            **hdf5_dataset_kwargs(array_results.shape, compression=compression),
 604        )
 605
 606        processed_dset.attrs["date_utc"] = timenow
 607
 608        processed_dset.attrs["ColumnsLabels"] = columns_labels
 609
 610        processed_dset.attrs["MoleculaSearchSetting"] = json.dumps(
 611            setting_dicts.get("MoleculaSearch"),
 612            sort_keys=False,
 613            indent=4,
 614            separators=(",", ": "),
 615        )
 616
 617        processed_dset.attrs["MassSpecPeakSetting"] = json.dumps(
 618            setting_dicts.get("MassSpecPeak"),
 619            sort_keys=False,
 620            indent=4,
 621            separators=(",", ": "),
 622        )
 623
 624        processed_dset.attrs["MassSpectrumSetting"] = json.dumps(
 625            setting_dicts.get("MassSpectrum"),
 626            sort_keys=False,
 627            indent=4,
 628            separators=(",", ": "),
 629        )
 630
 631    def to_hdf(self, compression=DEFAULT_HDF5_COMPRESSION, profile_dtype="f4"):
 632        """Exports the mass spectrum data to an HDF5 file.
 633
 634        Parameters
 635        ----------
 636        compression : int, str or None, optional
 637            Compression for the written datasets: a gzip level 0-9,
 638            ``"lzf"``, or ``None``/``"none"``. Default is 4. ``"lzf"`` is
 639            h5py's own filter, not a standard HDF5 one -- it needs h5py (or
 640            a tool with the lzf plugin) to read the result back.
 641        profile_dtype : str, optional
 642            dtype used for the stored profile m/z/abundance arrays
 643            (``"f4"`` or ``"f8"``). Default is ``"f4"``.
 644        """
 645
 646        with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle:
 647            self.add_mass_spectrum_to_hdf5(
 648                hdf_handle,
 649                self.mass_spectrum,
 650                str(self.mass_spectrum.scan_number),
 651                compression=compression,
 652                profile_dtype=profile_dtype,
 653            )
 654
 655    def parameters_to_toml(self):
 656        """Converts the mass spectrum parameters to a TOML string.
 657
 658        Returns
 659        -------
 660        str
 661            The TOML string of the mass spectrum parameters.
 662        """
 663
 664        dict_setting = parameter_to_dict.get_dict_data_ms(self.mass_spectrum)
 665
 666        dict_setting["MassSpecAttrs"] = self.get_mass_spec_attrs(self.mass_spectrum)
 667        dict_setting["analyzer"] = self.mass_spectrum.analyzer
 668        dict_setting["instrument_label"] = self.mass_spectrum.instrument_label
 669        dict_setting["sample_name"] = self.mass_spectrum.sample_name
 670
 671        output = _dumps_toml(dict_setting)
 672
 673        return output
 674
 675    def parameters_to_json(self):
 676        """Converts the mass spectrum parameters to a JSON string.
 677
 678        Returns
 679        -------
 680        str
 681            The JSON string of the mass spectrum parameters.
 682        """
 683
 684        dict_setting = parameter_to_dict.get_dict_data_ms(self.mass_spectrum)
 685
 686        dict_setting["MassSpecAttrs"] = self.get_mass_spec_attrs(self.mass_spectrum)
 687        dict_setting["analyzer"] = self.mass_spectrum.analyzer
 688        dict_setting["instrument_label"] = self.mass_spectrum.instrument_label
 689        dict_setting["sample_name"] = self.mass_spectrum.sample_name
 690
 691        output = json.dumps(dict_setting)
 692
 693        return output
 694
 695    def get_mass_spec_attrs(self, mass_spectrum):
 696        """Returns the mass spectrum attributes as a dictionary.
 697
 698        Parameters
 699        ----------
 700        mass_spectrum : MassSpectrum
 701            The mass spectrum to export.
 702
 703        Returns
 704        -------
 705        dict
 706            The mass spectrum attributes.
 707        """
 708
 709        dict_ms_attrs = {}
 710        dict_ms_attrs["polarity"] = mass_spectrum.polarity
 711        dict_ms_attrs["rt"] = mass_spectrum.retention_time
 712        dict_ms_attrs["tic"] = mass_spectrum.tic
 713        dict_ms_attrs["mobility_scan"] = mass_spectrum.mobility_scan
 714        dict_ms_attrs["mobility_rt"] = mass_spectrum.mobility_rt
 715        dict_ms_attrs["Aterm"] = mass_spectrum.Aterm
 716        dict_ms_attrs["Bterm"] = mass_spectrum.Bterm
 717        dict_ms_attrs["Cterm"] = mass_spectrum.Cterm
 718        dict_ms_attrs["baseline_noise"] = mass_spectrum.baseline_noise
 719        dict_ms_attrs["baseline_noise_std"] = mass_spectrum.baseline_noise_std
 720
 721        return dict_ms_attrs
 722
 723    def get_all_used_atoms_in_order(self, mass_spectrum):
 724        """Returns the list of assigned atoms in the order specified by Atoms.atoms_order list.
 725
 726        Parameters
 727        ----------
 728        mass_spectrum : MassSpectrum
 729            The mass spectrum to export.
 730
 731        Returns
 732        -------
 733        list
 734            The list of assigned atoms in the order specified by Atoms.atoms_order list.
 735        """
 736
 737        atoms_in_order = Atoms.atoms_order
 738        all_used_atoms = set()
 739        if mass_spectrum:
 740            for ms_peak in mass_spectrum:
 741                if ms_peak:
 742                    for m_formula in ms_peak:
 743                        for atom in m_formula.atoms:
 744                            all_used_atoms.add(atom)
 745
 746        def sort_method(atom):
 747            return [atoms_in_order.index(atom)]
 748
 749        return sorted(all_used_atoms, key=sort_method)
 750
 751    def list_dict_to_list(self, mass_spectrum, is_hdf5=False):
 752        """Returns the mass spectrum data as a list of dictionaries.
 753
 754        Parameters
 755        ----------
 756        mass_spectrum : MassSpectrum
 757            The mass spectrum to export.
 758        is_hdf5 : bool, optional
 759            Whether the mass spectrum is being exported to an HDF5 file. Defaults to False.
 760
 761        Returns
 762        -------
 763        list
 764            The mass spectrum data as a list of dictionaries.
 765        """
 766
 767        column_labels = self.columns_label + self.get_all_used_atoms_in_order(
 768            mass_spectrum
 769        )
 770
 771        dict_list = self.get_list_dict_data(mass_spectrum, is_hdf5=is_hdf5)
 772
 773        all_lines = []
 774        for dict_res in dict_list:
 775            result_line = [NaN] * len(column_labels)
 776
 777            for label, value in dict_res.items():
 778                label_index = column_labels.index(label)
 779                result_line[label_index] = value
 780
 781            all_lines.append(result_line)
 782
 783        return all_lines
 784
 785    def get_list_dict_data(
 786        self,
 787        mass_spectrum,
 788        include_no_match=True,
 789        include_isotopologues=True,
 790        isotopologue_inline=True,
 791        no_match_inline=False,
 792        is_hdf5=False,
 793        additional_columns=None,
 794    ):
 795        """Returns the mass spectrum data as a list of dictionaries.
 796
 797        Parameters
 798        ----------
 799        mass_spectrum : MassSpectrum
 800            The mass spectrum to export.
 801        include_no_match : bool, optional
 802            Whether to include unassigned (no match) data. Defaults to True.
 803        include_isotopologues : bool, optional
 804            Whether to include isotopologues. Defaults to True.
 805        isotopologue_inline : bool, optional
 806            Whether to include isotopologues inline. Defaults to True.
 807        no_match_inline : bool, optional
 808            Whether to include unassigned (no match) data inline. Defaults to False.
 809        is_hdf5 : bool, optional
 810            Whether the mass spectrum is being exported to an HDF5 file. Defaults to False.
 811
 812        Returns
 813        -------
 814        list
 815            The mass spectrum data as a list of dictionaries.
 816        """
 817
 818        dict_data_list = []
 819
 820        if is_hdf5:
 821            encode = ".encode('utf-8')"
 822        else:
 823            encode = ""
 824
 825        def add_no_match_dict_data(index, ms_peak):
 826            """
 827            Export dictionary of mspeak info for unassigned (no match) data
 828            """
 829            dict_result = {
 830                "Index": index,
 831                "m/z": ms_peak._mz_exp,
 832                "Calibrated m/z": ms_peak.mz_exp,
 833                "Peak Height": ms_peak.abundance,
 834                "Peak Area": ms_peak.area,
 835                "Resolving Power": ms_peak.resolving_power,
 836                "S/N": ms_peak.signal_to_noise,
 837                # Unassigned: polarity only (±1); no formula charge exists yet
 838                "Ion Charge": ms_peak.polarity,
 839                "Heteroatom Class": eval("Labels.unassigned{}".format(encode)),
 840            }
 841
 842            dict_data_list.append(dict_result)
 843
 844        def add_match_dict_data(index, ms_peak, mformula, additional_columns=None):
 845            """
 846            Export dictionary of mspeak info for assigned (match) data
 847            """
 848            formula_dict = mformula.to_dict()
 849
 850            dict_result = {
 851                "Index": index,
 852                "m/z": ms_peak._mz_exp,
 853                "Calibrated m/z": ms_peak.mz_exp,
 854                "Calculated m/z": mformula.mz_calc,
 855                "Peak Height": ms_peak.abundance,
 856                "Peak Area": ms_peak.area,
 857                "Resolving Power": ms_peak.resolving_power,
 858                "S/N": ms_peak.signal_to_noise,
 859                # Assigned: formula charge state (may be multi-charge); not peak polarity
 860                "Ion Charge": mformula.ion_charge,
 861                "m/z Error (ppm)": mformula.mz_error,
 862                "Confidence Score": mformula.confidence_score,
 863                "Isotopologue Similarity": mformula.isotopologue_similarity,
 864                "m/z Error Score": mformula.average_mz_error_score,
 865                "DBE": mformula.dbe,
 866                "Heteroatom Class": eval("mformula.class_label{}".format(encode)),
 867                "H/C": mformula.H_C,
 868                "O/C": mformula.O_C,
 869                "Ion Type": eval("mformula.ion_type.lower(){}".format(encode)),
 870                "Is Isotopologue": int(mformula.is_isotopologue),
 871                "Molecular Formula": eval("mformula.string{}".format(encode)),
 872            }
 873            if additional_columns is not None:
 874                possible_dict = {
 875                    "Aromaticity Index": mformula.A_I,
 876                    "NOSC": mformula.nosc,
 877                    "Aromaticity Index (modified)": mformula.A_I_mod,
 878                }
 879                for column in additional_columns:
 880                    dict_result[column] = possible_dict.get(column)
 881
 882            if mformula.adduct_atom:
 883                dict_result["Adduct"] = eval("mformula.adduct_atom{}".format(encode))
 884
 885            if mformula.is_isotopologue:
 886                dict_result["Mono Isotopic Index"] = mformula.mspeak_index_mono_isotopic
 887
 888            if self.atoms_order_list is None:
 889                atoms_order_list = self.get_all_used_atoms_in_order(mass_spectrum)
 890            else:
 891                atoms_order_list = self.atoms_order_list
 892
 893            for atom in atoms_order_list:
 894                if atom in formula_dict.keys():
 895                    dict_result[atom] = formula_dict.get(atom)
 896
 897            dict_data_list.append(dict_result)
 898
 899        score_methods = mass_spectrum.molecular_search_settings.score_methods
 900        selected_score_method = (
 901            mass_spectrum.molecular_search_settings.output_score_method
 902        )
 903
 904        if selected_score_method in score_methods:
 905            # temp set score method as the one chosen in the output
 906            current_method = mass_spectrum.molecular_search_settings.score_method
 907            mass_spectrum.molecular_search_settings.score_method = selected_score_method
 908
 909            for index, ms_peak in enumerate(mass_spectrum):
 910                # print(ms_peak.mz_exp)
 911
 912                if ms_peak:
 913                    m_formula = ms_peak.best_molecular_formula_candidate
 914
 915                    if m_formula:
 916                        if not m_formula.is_isotopologue:
 917                            add_match_dict_data(
 918                                index,
 919                                ms_peak,
 920                                m_formula,
 921                                additional_columns=additional_columns,
 922                            )
 923
 924                            for (
 925                                iso_mspeak_index,
 926                                iso_mf_formula,
 927                            ) in m_formula.mspeak_mf_isotopologues_indexes:
 928                                iso_ms_peak = mass_spectrum[iso_mspeak_index]
 929                                add_match_dict_data(
 930                                    iso_mspeak_index,
 931                                    iso_ms_peak,
 932                                    iso_mf_formula,
 933                                    additional_columns=additional_columns,
 934                                )
 935                else:
 936                    if include_no_match and no_match_inline:
 937                        add_no_match_dict_data(index, ms_peak)
 938
 939            if include_no_match and not no_match_inline:
 940                for index, ms_peak in enumerate(mass_spectrum):
 941                    if not ms_peak:
 942                        add_no_match_dict_data(index, ms_peak)
 943            # reset score method as the one chosen in the output
 944            mass_spectrum.molecular_search_settings.score_method = current_method
 945
 946        else:
 947            for index, ms_peak in enumerate(mass_spectrum):
 948                # check if there is a molecular formula candidate for the msPeak
 949
 950                if ms_peak:
 951                    # m_formula = ms_peak.molecular_formula_lowest_error
 952                    for m_formula in ms_peak:
 953                        if mass_spectrum.molecular_search_settings.output_min_score > 0:
 954                            if (
 955                                m_formula.confidence_score
 956                                >= mass_spectrum.molecular_search_settings.output_min_score
 957                            ):
 958                                if m_formula.is_isotopologue:  # isotopologues inline
 959                                    if include_isotopologues and isotopologue_inline:
 960                                        add_match_dict_data(
 961                                            index,
 962                                            ms_peak,
 963                                            m_formula,
 964                                            additional_columns=additional_columns,
 965                                        )
 966                                else:
 967                                    add_match_dict_data(
 968                                        index,
 969                                        ms_peak,
 970                                        m_formula,
 971                                        additional_columns=additional_columns,
 972                                    )  # add monoisotopic peak
 973
 974                            # cutoff because of low score
 975                            else:
 976                                add_no_match_dict_data(index, ms_peak)
 977
 978                        else:
 979                            if m_formula.is_isotopologue:  # isotopologues inline
 980                                if include_isotopologues and isotopologue_inline:
 981                                    add_match_dict_data(
 982                                        index,
 983                                        ms_peak,
 984                                        m_formula,
 985                                        additional_columns=additional_columns,
 986                                    )
 987                            else:
 988                                add_match_dict_data(
 989                                    index,
 990                                    ms_peak,
 991                                    m_formula,
 992                                    additional_columns=additional_columns,
 993                                )  # add monoisotopic peak
 994                else:
 995                    # include not_match
 996                    if include_no_match and no_match_inline:
 997                        add_no_match_dict_data(index, ms_peak)
 998
 999            if include_isotopologues and not isotopologue_inline:
1000                for index, ms_peak in enumerate(mass_spectrum):
1001                    for m_formula in ms_peak:
1002                        if m_formula.is_isotopologue:
1003                            if (
1004                                m_formula.confidence_score
1005                                >= mass_spectrum.molecular_search_settings.output_min_score
1006                            ):
1007                                add_match_dict_data(
1008                                    index,
1009                                    ms_peak,
1010                                    m_formula,
1011                                    additional_columns=additional_columns,
1012                                )
1013
1014            if include_no_match and not no_match_inline:
1015                for index, ms_peak in enumerate(mass_spectrum):
1016                    if not ms_peak:
1017                        add_no_match_dict_data(index, ms_peak)
1018
1019        # remove duplicated add_match data possibly introduced on the output_score_filter step
1020        res = []
1021        [res.append(x) for x in dict_data_list if x not in res]
1022
1023        return res

A class for exporting high-resolution mass spectra.

Parameters
  • out_file_path (str): The output file path.
  • mass_spectrum (MassSpectrum): The mass spectrum to export.
  • output_type (str, optional): The type of output file. Defaults to 'excel'. Can be 'excel', 'csv', 'pandas', 'parquet' or 'hdf5'.
Attributes
  • output_file (Path): The output file path.
  • output_type (str): The type of output file.
  • mass_spectrum (MassSpectrum): The mass spectrum to export.
  • atoms_order_list (list): The list of assigned atoms in the order specified by Atoms.atoms_order list.
  • columns_label (list): The column labels in order.
Methods
  • save(). Save the mass spectrum data to the output file.
  • run(). Run the export process.
  • get_pandas_df(). Returns the mass spectrum data as a pandas DataFrame.
  • write_settings(output_path, mass_spectrum). Writes the settings of the mass spectrum to a JSON file.
  • to_pandas(write_metadata=True). Exports the mass spectrum data to a pandas DataFrame and saves it as a pickle file.
  • to_excel(write_metadata=True). Exports the mass spectrum data to an Excel file.
  • to_csv(write_metadata=True). Exports the mass spectrum data to a CSV file.
  • to_parquet(write_metadata=True). Exports the mass spectrum data to a Parquet file.
  • to_json(). Exports the mass spectrum data to a JSON string.
  • to_hdf(). Exports the mass spectrum data to an HDF5 file.
  • parameters_to_toml(). Converts the mass spectrum parameters to a TOML string.
  • parameters_to_json(). Converts the mass spectrum parameters to a JSON string.
  • get_mass_spec_attrs(mass_spectrum). Returns the mass spectrum attributes as a dictionary.
  • get_all_used_atoms_in_order(mass_spectrum). Returns the list of assigned atoms in the order specified by Atoms.atoms_order list.
  • list_dict_to_list(mass_spectrum, is_hdf5=False). Returns the mass spectrum data as a list of dictionaries.
  • get_list_dict_data(mass_spectrum, include_no_match=True, include_isotopologues=True, isotopologue_inline=True, no_match_inline=False, is_hdf5=False). Returns the mass spectrum data as a list of dictionaries.
HighResMassSpecExport(out_file_path, mass_spectrum, output_type='excel')
138    def __init__(self, out_file_path, mass_spectrum, output_type="excel"):
139        Thread.__init__(self)
140
141        self.output_file = Path(out_file_path)
142
143        # 'excel', 'csv' or 'pandas'
144        self.output_type = output_type
145
146        self.mass_spectrum = mass_spectrum
147
148        # collect all assigned atoms and order them accordingly to the Atoms.atoms_order list
149        self.atoms_order_list = self.get_all_used_atoms_in_order(self.mass_spectrum)
150
151        self._init_columns()

This constructor should always be called with keyword arguments. Arguments are:

group should be None; reserved for future extension when a ThreadGroup class is implemented.

target is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called.

name is the thread name. By default, a unique name is constructed of the form "Thread-N" where N is a small decimal number.

args is a list or tuple of arguments for the target invocation. Defaults to ().

kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}.

If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread.

output_file
output_type
181    @property
182    def output_type(self):
183        """Returns the output type of the mass spectrum."""
184        return self._output_type

Returns the output type of the mass spectrum.

mass_spectrum
atoms_order_list
def save(self):
197    def save(self):
198        """Save the mass spectrum data to the output file.
199
200        Raises
201        ------
202        ValueError
203            If the output type is not supported.
204        """
205
206        if self.output_type == "excel":
207            self.to_excel()
208        elif self.output_type == "csv":
209            self.to_csv()
210        elif self.output_type == "pandas":
211            self.to_pandas()
212        elif self.output_type == "parquet":
213            self.to_parquet()
214        elif self.output_type == "hdf5":
215            self.to_hdf()
216        else:
217            raise ValueError(
218                "Unkown output type: %s; it can be 'excel', 'csv', 'pandas', 'parquet' or 'hdf5'"
219                % self.output_type
220            )

Save the mass spectrum data to the output file.

Raises
  • ValueError: If the output type is not supported.
def run(self):
222    def run(self):
223        """Run the export process.
224
225        This method is called when the thread starts.
226        It calls the save method to perform the export."""
227        self.save()

Run the export process.

This method is called when the thread starts. It calls the save method to perform the export.

def get_pandas_df(self, additional_columns=None):
229    def get_pandas_df(self, additional_columns=None):
230        """Returns the mass spectrum data as a pandas DataFrame.
231
232        Parameters
233        ----------
234        additional_columns : list, optional
235            Additional columns to include in the DataFrame. Defaults to None.
236            Suitable additional columns are: 'Aromaticity Index', 'NOSC', 'Aromaticity Index (modified)'.
237
238        Returns
239        -------
240        DataFrame
241            The mass spectrum data as a pandas DataFrame.
242        """
243        if additional_columns is not None:
244            possible_additional_columns = [
245                "Aromaticity Index",
246                "NOSC",
247                "Aromaticity Index (modified)",
248            ]
249            if additional_columns:
250                for column in additional_columns:
251                    if column not in possible_additional_columns:
252                        raise ValueError("Invalid additional column: %s" % column)
253            columns = (
254                self.columns_label
255                + additional_columns
256                + self.get_all_used_atoms_in_order(self.mass_spectrum)
257            )
258        else:
259            columns = self.columns_label + self.get_all_used_atoms_in_order(
260                self.mass_spectrum
261            )
262        dict_data_list = self.get_list_dict_data(
263            self.mass_spectrum, additional_columns=additional_columns
264        )
265        df = DataFrame(dict_data_list, columns=columns)
266        df.attrs["name"] = self.output_file
267        return df

Returns the mass spectrum data as a pandas DataFrame.

Parameters
  • additional_columns (list, optional): Additional columns to include in the DataFrame. Defaults to None. Suitable additional columns are: 'Aromaticity Index', 'NOSC', 'Aromaticity Index (modified)'.
Returns
  • DataFrame: The mass spectrum data as a pandas DataFrame.
def write_settings(self, output_path, mass_spectrum):
269    def write_settings(self, output_path, mass_spectrum):
270        """Writes the settings of the mass spectrum to a JSON file.
271
272        Parameters
273        ----------
274        output_path : str
275            The output file path.
276        mass_spectrum : MassSpectrum
277            The mass spectrum to export.
278        """
279
280        import json
281
282        dict_setting = parameter_to_dict.get_dict_data_ms(mass_spectrum)
283
284        dict_setting["MassSpecAttrs"] = self.get_mass_spec_attrs(mass_spectrum)
285        dict_setting["analyzer"] = mass_spectrum.analyzer
286        dict_setting["instrument_label"] = mass_spectrum.instrument_label
287        dict_setting["sample_name"] = mass_spectrum.sample_name
288
289        with open(
290            output_path.with_suffix(".json"),
291            "w",
292            encoding="utf8",
293        ) as outfile:
294            output = json.dumps(
295                dict_setting, sort_keys=True, indent=4, separators=(",", ": ")
296            )
297            outfile.write(output)

Writes the settings of the mass spectrum to a JSON file.

Parameters
  • output_path (str): The output file path.
  • mass_spectrum (MassSpectrum): The mass spectrum to export.
def to_pandas(self, write_metadata=True):
299    def to_pandas(self, write_metadata=True):
300        """Exports the mass spectrum data to a pandas DataFrame and saves it as a pickle file.
301
302        Parameters
303        ----------
304        write_metadata : bool, optional
305            Whether to write the metadata to a JSON file. Defaults to True.
306        """
307
308        columns = self.columns_label + self.get_all_used_atoms_in_order(
309            self.mass_spectrum
310        )
311
312        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
313
314        df = DataFrame(dict_data_list, columns=columns)
315
316        df.to_pickle(self.output_file.with_suffix(".pkl"))
317
318        if write_metadata:
319            self.write_settings(self.output_file, self.mass_spectrum)

Exports the mass spectrum data to a pandas DataFrame and saves it as a pickle file.

Parameters
  • write_metadata (bool, optional): Whether to write the metadata to a JSON file. Defaults to True.
def to_excel(self, write_metadata=True):
321    def to_excel(self, write_metadata=True):
322        """Exports the mass spectrum data to an Excel file.
323
324        Parameters
325        ----------
326        write_metadata : bool, optional
327            Whether to write the metadata to a JSON file. Defaults to True.
328        """
329
330        columns = self.columns_label + self.get_all_used_atoms_in_order(
331            self.mass_spectrum
332        )
333
334        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
335
336        df = DataFrame(dict_data_list, columns=columns)
337
338        df.to_excel(self.output_file.with_suffix(".xlsx"))
339
340        if write_metadata:
341            self.write_settings(self.output_file, self.mass_spectrum)

Exports the mass spectrum data to an Excel file.

Parameters
  • write_metadata (bool, optional): Whether to write the metadata to a JSON file. Defaults to True.
def to_csv(self, write_metadata=True):
343    def to_csv(self, write_metadata=True):
344        """Exports the mass spectrum data to a CSV file.
345
346        Parameters
347        ----------
348        write_metadata : bool, optional
349            Whether to write the metadata to a JSON file. Defaults to True.
350        """
351
352        columns = self.columns_label + self.get_all_used_atoms_in_order(
353            self.mass_spectrum
354        )
355
356        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
357
358        import csv
359
360        try:
361            with open(self.output_file.with_suffix(".csv"), "w", newline="") as csvfile:
362                writer = csv.DictWriter(csvfile, fieldnames=columns)
363                writer.writeheader()
364                for data in dict_data_list:
365                    writer.writerow(data)
366            if write_metadata:
367                self.write_settings(self.output_file, self.mass_spectrum)
368
369        except IOError as ioerror:
370            print(ioerror)

Exports the mass spectrum data to a CSV file.

Parameters
  • write_metadata (bool, optional): Whether to write the metadata to a JSON file. Defaults to True.
def to_parquet(self, write_metadata=True):
372    def to_parquet(self, write_metadata=True):
373        """Exports the mass spectrum data to a Parquet file.
374
375        Parameters
376        ----------
377        write_metadata : bool, optional
378            Whether to write the metadata to a JSON file. Defaults to True.
379
380        Notes
381        -----
382        Requires ``pyarrow`` (or ``fastparquet``) for pandas Parquet I/O.
383        """
384
385        columns = self.columns_label + self.get_all_used_atoms_in_order(
386            self.mass_spectrum
387        )
388
389        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
390
391        df = DataFrame(dict_data_list, columns=columns)
392
393        df.to_parquet(self.output_file.with_suffix(".parquet"), index=False)
394
395        if write_metadata:
396            self.write_settings(self.output_file, self.mass_spectrum)

Exports the mass spectrum data to a Parquet file.

Parameters
  • write_metadata (bool, optional): Whether to write the metadata to a JSON file. Defaults to True.
Notes

Requires pyarrow (or fastparquet) for pandas Parquet I/O.

def to_json(self):
398    def to_json(self):
399        """Exports the mass spectrum data to a JSON string."""
400
401        columns = self.columns_label + self.get_all_used_atoms_in_order(
402            self.mass_spectrum
403        )
404
405        dict_data_list = self.get_list_dict_data(self.mass_spectrum)
406
407        df = DataFrame(dict_data_list, columns=columns)
408
409        # for key, values in dict_data.items():
410        #    if not values: dict_data[key] = NaN
411
412        # output = json.dumps(dict_data, sort_keys=True, indent=4, separators=(',', ': '))
413        return df.to_json(orient="records")

Exports the mass spectrum data to a JSON string.

def add_mass_spectrum_to_hdf5( self, hdf_handle, mass_spectrum, group_key, mass_spectra_group=None, export_raw=True, compression=4, profile_dtype='f4'):
415    def add_mass_spectrum_to_hdf5(
416        self,
417        hdf_handle,
418        mass_spectrum,
419        group_key,
420        mass_spectra_group=None,
421        export_raw=True,
422        compression=DEFAULT_HDF5_COMPRESSION,
423        profile_dtype="f4",
424    ):
425        """Adds the mass spectrum data to an HDF5 file.
426
427        Parameters
428        ----------
429        hdf_handle : h5py.File
430            The HDF5 file handle.
431        mass_spectrum : MassSpectrum
432            The mass spectrum to add to the HDF5 file.
433        group_key : str
434            The group key (where to add the mass spectrum data within the HDF5 file).
435        mass_spectra_group : h5py.Group, optional
436            The mass spectra group. Defaults to None (no group, mass spectrum is added to the root).
437        export_raw : bool, optional
438            Whether to export the raw data. Defaults to True.
439            If False, only the processed data (peaks) is exported (essentially centroided data).
440        compression : int, str or None, optional
441            Compression for the written datasets: a gzip level 0-9, ``"lzf"``,
442            or ``None``/``"none"``. Default is 4. ``"lzf"`` is h5py's own
443            filter, not a standard HDF5 one -- it needs h5py (or a tool
444            with the lzf plugin) to read the result back.
445        profile_dtype : str, optional
446            dtype used for the stored profile m/z/abundance arrays
447            (``"f4"`` or ``"f8"``). Default is ``"f4"``.
448        """
449        if mass_spectra_group is None:
450            # Check if the file has the necessary attributes and add them if not
451            # This assumes that if there is a mass_spectra_group, these attributes were already added to the file
452            if not hdf_handle.attrs.get("date_utc"):
453                timenow = str(
454                    datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z")
455                )
456                hdf_handle.attrs["date_utc"] = timenow
457                hdf_handle.attrs["file_name"] = mass_spectrum.filename.name
458                hdf_handle.attrs["data_structure"] = "mass_spectrum"
459                hdf_handle.attrs["analyzer"] = mass_spectrum.analyzer
460                hdf_handle.attrs["instrument_label"] = mass_spectrum.instrument_label
461                hdf_handle.attrs["sample_name"] = mass_spectrum.sample_name
462
463        list_results = self.list_dict_to_list(mass_spectrum, is_hdf5=True)
464
465        dict_ms_attrs = self.get_mass_spec_attrs(mass_spectrum)
466
467        setting_dicts = parameter_to_dict.get_dict_data_ms(mass_spectrum)
468
469        columns_labels = json.dumps(
470            self.columns_label + self.get_all_used_atoms_in_order(mass_spectrum),
471            sort_keys=False,
472            indent=4,
473            separators=(",", ": "),
474        )
475
476        group_key = group_key
477
478        if mass_spectra_group is not None:
479            hdf_handle = mass_spectra_group
480
481        if group_key not in hdf_handle.keys():
482            scan_group = hdf_handle.create_group(group_key)
483
484            # If there is raw data (from profile data) save it
485            if not mass_spectrum.is_centroid and export_raw:
486                # "mz" is always the experimental (uncalibrated) profile; a
487                # separate "mz_cal" row is added only when the spectrum is
488                # actually calibrated. mz_cal_profile alone isn't enough to
489                # tell: MzDomainCalibration.__init__ pre-populates it before
490                # any calibration has run, so a never-calibrated spectrum
491                # can still have a (stale/placeholder) mz_cal_profile.
492                row_labels = ["mz", "abundance"]
493                rows = [
494                    np.asarray(mass_spectrum._mz_exp),
495                    np.asarray(mass_spectrum.abundance_profile),
496                ]
497                mz_cal_profile = mass_spectrum.mz_cal_profile
498                if mass_spectrum.is_calibrated and mz_cal_profile is not None:
499                    row_labels.append("mz_cal")
500                    rows.append(np.asarray(mz_cal_profile))
501
502                mz_abun_array = np.array(rows, dtype=profile_dtype)
503
504                raw_ms_dataset = scan_group.create_dataset(
505                    "raw_ms",
506                    data=mz_abun_array,
507                    dtype=profile_dtype,
508                    **hdf5_dataset_kwargs(mz_abun_array.shape, compression=compression),
509                )
510                raw_ms_dataset.attrs["row_labels"] = json.dumps(row_labels)
511                # Recorded independently of whether an "mz_cal" row was
512                # written, so a reader can trust this over row presence.
513                raw_ms_dataset.attrs["is_calibrated"] = bool(
514                    mass_spectrum.is_calibrated
515                )
516
517                # Add metadata to the raw_ms dataset when it exists
518                raw_ms_dataset.attrs["MassSpecAttrs"] = json.dumps(dict_ms_attrs)
519
520                if isinstance(mass_spectrum, MassSpecfromFreq):
521                    raw_ms_dataset.attrs["TransientSetting"] = json.dumps(
522                        setting_dicts.get("TransientSetting"),
523                        sort_keys=False,
524                        indent=4,
525                        separators=(",", ": "),
526                    )
527            else:
528                # For centroided data, store metadata directly on the scan group
529                scan_group.attrs["MassSpecAttrs"] = json.dumps(dict_ms_attrs)
530
531                if isinstance(mass_spectrum, MassSpecfromFreq):
532                    scan_group.attrs["TransientSetting"] = json.dumps(
533                        setting_dicts.get("TransientSetting"),
534                        sort_keys=False,
535                        indent=4,
536                        separators=(",", ": "),
537                    )
538
539        else:
540            scan_group = hdf_handle.get(group_key)
541
542        # Next processed-data index: one past the highest existing numeric
543        # key, or 0 if there are none. "raw_ms" and "peak_profile_indices_*"
544        # are not processed-data indices (get_time_index_to_pull also only
545        # considers numeric keys, for the same reason). Using the max rather
546        # than a count matters for a legacy file that already has "raw_ms"
547        # and "1" (an old off-by-one: pre-fix code counted "raw_ms" as an
548        # occupied slot) -- counting numeric keys would give "1" again and
549        # collide with the existing one; max+1 gives "2".
550        numeric_keys = [int(k) for k in scan_group.keys() if k.isdigit()]
551        index_processed_data = str(max(numeric_keys) + 1) if numeric_keys else "0"
552
553        # Per-peak (left, apex, right) indices into the profile arrays above,
554        # in mspeaks order, so a reloaded centroid-with-profile mass
555        # spectrum doesn't need to re-derive peak boundaries from the peak
556        # table's row position (which isn't a profile index). Written once
557        # per peak-table version (same suffix as the processed-data index
558        # below), since the profile itself (raw_ms) doesn't change across
559        # reprocessing but the picked peaks do -- except raw_ms itself is
560        # only ever written on the group's first creation (just above), so
561        # if *this* mass_spectrum's profile is a different length (e.g. a
562        # later reprocessing that re-picked over different raw data), these
563        # indices would point into a profile that was never stored. Skip
564        # writing them in that case; the reader falls back to a search.
565        existing_raw_ms = scan_group.get("raw_ms")
566        if (
567            not mass_spectrum.is_centroid
568            and export_raw
569            and existing_raw_ms is not None
570            and existing_raw_ms.shape[-1] == len(mass_spectrum.abundance_profile)
571        ):
572            peak_indices = np.array(
573                [
574                    (
575                        mspeak.peak_left_index,
576                        mspeak.peak_apex_index,
577                        mspeak.peak_right_index,
578                    )
579                    for mspeak in mass_spectrum.mspeaks
580                ],
581                dtype=np.int32,
582            ).reshape(-1, 3)
583            scan_group.create_dataset(
584                f"peak_profile_indices_{index_processed_data}",
585                data=peak_indices,
586                **hdf5_dataset_kwargs(peak_indices.shape, compression=compression),
587            )
588
589        timenow = str(datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z"))
590
591        # Convert list_results to numpy array and optimize data types
592        array_results = np.array(list_results)
593
594        # Apply data type optimization for numeric columns
595        if array_results.dtype == np.float64:
596            array_results = array_results.astype(np.float32)
597        elif array_results.dtype == np.int64:
598            array_results = array_results.astype(np.int32)
599
600        processed_dset = scan_group.create_dataset(
601            index_processed_data,
602            data=array_results,
603            **hdf5_dataset_kwargs(array_results.shape, compression=compression),
604        )
605
606        processed_dset.attrs["date_utc"] = timenow
607
608        processed_dset.attrs["ColumnsLabels"] = columns_labels
609
610        processed_dset.attrs["MoleculaSearchSetting"] = json.dumps(
611            setting_dicts.get("MoleculaSearch"),
612            sort_keys=False,
613            indent=4,
614            separators=(",", ": "),
615        )
616
617        processed_dset.attrs["MassSpecPeakSetting"] = json.dumps(
618            setting_dicts.get("MassSpecPeak"),
619            sort_keys=False,
620            indent=4,
621            separators=(",", ": "),
622        )
623
624        processed_dset.attrs["MassSpectrumSetting"] = json.dumps(
625            setting_dicts.get("MassSpectrum"),
626            sort_keys=False,
627            indent=4,
628            separators=(",", ": "),
629        )

Adds the mass spectrum data to an HDF5 file.

Parameters
  • hdf_handle (h5py.File): The HDF5 file handle.
  • mass_spectrum (MassSpectrum): The mass spectrum to add to the HDF5 file.
  • group_key (str): The group key (where to add the mass spectrum data within the HDF5 file).
  • mass_spectra_group (h5py.Group, optional): The mass spectra group. Defaults to None (no group, mass spectrum is added to the root).
  • export_raw (bool, optional): Whether to export the raw data. Defaults to True. If False, only the processed data (peaks) is exported (essentially centroided data).
  • compression (int, str or None, optional): Compression for the written datasets: a gzip level 0-9, "lzf", or None/"none". Default is 4. "lzf" is h5py's own filter, not a standard HDF5 one -- it needs h5py (or a tool with the lzf plugin) to read the result back.
  • profile_dtype (str, optional): dtype used for the stored profile m/z/abundance arrays ("f4" or "f8"). Default is "f4".
def to_hdf(self, compression=4, profile_dtype='f4'):
631    def to_hdf(self, compression=DEFAULT_HDF5_COMPRESSION, profile_dtype="f4"):
632        """Exports the mass spectrum data to an HDF5 file.
633
634        Parameters
635        ----------
636        compression : int, str or None, optional
637            Compression for the written datasets: a gzip level 0-9,
638            ``"lzf"``, or ``None``/``"none"``. Default is 4. ``"lzf"`` is
639            h5py's own filter, not a standard HDF5 one -- it needs h5py (or
640            a tool with the lzf plugin) to read the result back.
641        profile_dtype : str, optional
642            dtype used for the stored profile m/z/abundance arrays
643            (``"f4"`` or ``"f8"``). Default is ``"f4"``.
644        """
645
646        with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle:
647            self.add_mass_spectrum_to_hdf5(
648                hdf_handle,
649                self.mass_spectrum,
650                str(self.mass_spectrum.scan_number),
651                compression=compression,
652                profile_dtype=profile_dtype,
653            )

Exports the mass spectrum data to an HDF5 file.

Parameters
  • compression (int, str or None, optional): Compression for the written datasets: a gzip level 0-9, "lzf", or None/"none". Default is 4. "lzf" is h5py's own filter, not a standard HDF5 one -- it needs h5py (or a tool with the lzf plugin) to read the result back.
  • profile_dtype (str, optional): dtype used for the stored profile m/z/abundance arrays ("f4" or "f8"). Default is "f4".
def parameters_to_toml(self):
655    def parameters_to_toml(self):
656        """Converts the mass spectrum parameters to a TOML string.
657
658        Returns
659        -------
660        str
661            The TOML string of the mass spectrum parameters.
662        """
663
664        dict_setting = parameter_to_dict.get_dict_data_ms(self.mass_spectrum)
665
666        dict_setting["MassSpecAttrs"] = self.get_mass_spec_attrs(self.mass_spectrum)
667        dict_setting["analyzer"] = self.mass_spectrum.analyzer
668        dict_setting["instrument_label"] = self.mass_spectrum.instrument_label
669        dict_setting["sample_name"] = self.mass_spectrum.sample_name
670
671        output = _dumps_toml(dict_setting)
672
673        return output

Converts the mass spectrum parameters to a TOML string.

Returns
  • str: The TOML string of the mass spectrum parameters.
def parameters_to_json(self):
675    def parameters_to_json(self):
676        """Converts the mass spectrum parameters to a JSON string.
677
678        Returns
679        -------
680        str
681            The JSON string of the mass spectrum parameters.
682        """
683
684        dict_setting = parameter_to_dict.get_dict_data_ms(self.mass_spectrum)
685
686        dict_setting["MassSpecAttrs"] = self.get_mass_spec_attrs(self.mass_spectrum)
687        dict_setting["analyzer"] = self.mass_spectrum.analyzer
688        dict_setting["instrument_label"] = self.mass_spectrum.instrument_label
689        dict_setting["sample_name"] = self.mass_spectrum.sample_name
690
691        output = json.dumps(dict_setting)
692
693        return output

Converts the mass spectrum parameters to a JSON string.

Returns
  • str: The JSON string of the mass spectrum parameters.
def get_mass_spec_attrs(self, mass_spectrum):
695    def get_mass_spec_attrs(self, mass_spectrum):
696        """Returns the mass spectrum attributes as a dictionary.
697
698        Parameters
699        ----------
700        mass_spectrum : MassSpectrum
701            The mass spectrum to export.
702
703        Returns
704        -------
705        dict
706            The mass spectrum attributes.
707        """
708
709        dict_ms_attrs = {}
710        dict_ms_attrs["polarity"] = mass_spectrum.polarity
711        dict_ms_attrs["rt"] = mass_spectrum.retention_time
712        dict_ms_attrs["tic"] = mass_spectrum.tic
713        dict_ms_attrs["mobility_scan"] = mass_spectrum.mobility_scan
714        dict_ms_attrs["mobility_rt"] = mass_spectrum.mobility_rt
715        dict_ms_attrs["Aterm"] = mass_spectrum.Aterm
716        dict_ms_attrs["Bterm"] = mass_spectrum.Bterm
717        dict_ms_attrs["Cterm"] = mass_spectrum.Cterm
718        dict_ms_attrs["baseline_noise"] = mass_spectrum.baseline_noise
719        dict_ms_attrs["baseline_noise_std"] = mass_spectrum.baseline_noise_std
720
721        return dict_ms_attrs

Returns the mass spectrum attributes as a dictionary.

Parameters
  • mass_spectrum (MassSpectrum): The mass spectrum to export.
Returns
  • dict: The mass spectrum attributes.
def get_all_used_atoms_in_order(self, mass_spectrum):
723    def get_all_used_atoms_in_order(self, mass_spectrum):
724        """Returns the list of assigned atoms in the order specified by Atoms.atoms_order list.
725
726        Parameters
727        ----------
728        mass_spectrum : MassSpectrum
729            The mass spectrum to export.
730
731        Returns
732        -------
733        list
734            The list of assigned atoms in the order specified by Atoms.atoms_order list.
735        """
736
737        atoms_in_order = Atoms.atoms_order
738        all_used_atoms = set()
739        if mass_spectrum:
740            for ms_peak in mass_spectrum:
741                if ms_peak:
742                    for m_formula in ms_peak:
743                        for atom in m_formula.atoms:
744                            all_used_atoms.add(atom)
745
746        def sort_method(atom):
747            return [atoms_in_order.index(atom)]
748
749        return sorted(all_used_atoms, key=sort_method)

Returns the list of assigned atoms in the order specified by Atoms.atoms_order list.

Parameters
  • mass_spectrum (MassSpectrum): The mass spectrum to export.
Returns
  • list: The list of assigned atoms in the order specified by Atoms.atoms_order list.
def list_dict_to_list(self, mass_spectrum, is_hdf5=False):
751    def list_dict_to_list(self, mass_spectrum, is_hdf5=False):
752        """Returns the mass spectrum data as a list of dictionaries.
753
754        Parameters
755        ----------
756        mass_spectrum : MassSpectrum
757            The mass spectrum to export.
758        is_hdf5 : bool, optional
759            Whether the mass spectrum is being exported to an HDF5 file. Defaults to False.
760
761        Returns
762        -------
763        list
764            The mass spectrum data as a list of dictionaries.
765        """
766
767        column_labels = self.columns_label + self.get_all_used_atoms_in_order(
768            mass_spectrum
769        )
770
771        dict_list = self.get_list_dict_data(mass_spectrum, is_hdf5=is_hdf5)
772
773        all_lines = []
774        for dict_res in dict_list:
775            result_line = [NaN] * len(column_labels)
776
777            for label, value in dict_res.items():
778                label_index = column_labels.index(label)
779                result_line[label_index] = value
780
781            all_lines.append(result_line)
782
783        return all_lines

Returns the mass spectrum data as a list of dictionaries.

Parameters
  • mass_spectrum (MassSpectrum): The mass spectrum to export.
  • is_hdf5 (bool, optional): Whether the mass spectrum is being exported to an HDF5 file. Defaults to False.
Returns
  • list: The mass spectrum data as a list of dictionaries.
def get_list_dict_data( self, mass_spectrum, include_no_match=True, include_isotopologues=True, isotopologue_inline=True, no_match_inline=False, is_hdf5=False, additional_columns=None):
 785    def get_list_dict_data(
 786        self,
 787        mass_spectrum,
 788        include_no_match=True,
 789        include_isotopologues=True,
 790        isotopologue_inline=True,
 791        no_match_inline=False,
 792        is_hdf5=False,
 793        additional_columns=None,
 794    ):
 795        """Returns the mass spectrum data as a list of dictionaries.
 796
 797        Parameters
 798        ----------
 799        mass_spectrum : MassSpectrum
 800            The mass spectrum to export.
 801        include_no_match : bool, optional
 802            Whether to include unassigned (no match) data. Defaults to True.
 803        include_isotopologues : bool, optional
 804            Whether to include isotopologues. Defaults to True.
 805        isotopologue_inline : bool, optional
 806            Whether to include isotopologues inline. Defaults to True.
 807        no_match_inline : bool, optional
 808            Whether to include unassigned (no match) data inline. Defaults to False.
 809        is_hdf5 : bool, optional
 810            Whether the mass spectrum is being exported to an HDF5 file. Defaults to False.
 811
 812        Returns
 813        -------
 814        list
 815            The mass spectrum data as a list of dictionaries.
 816        """
 817
 818        dict_data_list = []
 819
 820        if is_hdf5:
 821            encode = ".encode('utf-8')"
 822        else:
 823            encode = ""
 824
 825        def add_no_match_dict_data(index, ms_peak):
 826            """
 827            Export dictionary of mspeak info for unassigned (no match) data
 828            """
 829            dict_result = {
 830                "Index": index,
 831                "m/z": ms_peak._mz_exp,
 832                "Calibrated m/z": ms_peak.mz_exp,
 833                "Peak Height": ms_peak.abundance,
 834                "Peak Area": ms_peak.area,
 835                "Resolving Power": ms_peak.resolving_power,
 836                "S/N": ms_peak.signal_to_noise,
 837                # Unassigned: polarity only (±1); no formula charge exists yet
 838                "Ion Charge": ms_peak.polarity,
 839                "Heteroatom Class": eval("Labels.unassigned{}".format(encode)),
 840            }
 841
 842            dict_data_list.append(dict_result)
 843
 844        def add_match_dict_data(index, ms_peak, mformula, additional_columns=None):
 845            """
 846            Export dictionary of mspeak info for assigned (match) data
 847            """
 848            formula_dict = mformula.to_dict()
 849
 850            dict_result = {
 851                "Index": index,
 852                "m/z": ms_peak._mz_exp,
 853                "Calibrated m/z": ms_peak.mz_exp,
 854                "Calculated m/z": mformula.mz_calc,
 855                "Peak Height": ms_peak.abundance,
 856                "Peak Area": ms_peak.area,
 857                "Resolving Power": ms_peak.resolving_power,
 858                "S/N": ms_peak.signal_to_noise,
 859                # Assigned: formula charge state (may be multi-charge); not peak polarity
 860                "Ion Charge": mformula.ion_charge,
 861                "m/z Error (ppm)": mformula.mz_error,
 862                "Confidence Score": mformula.confidence_score,
 863                "Isotopologue Similarity": mformula.isotopologue_similarity,
 864                "m/z Error Score": mformula.average_mz_error_score,
 865                "DBE": mformula.dbe,
 866                "Heteroatom Class": eval("mformula.class_label{}".format(encode)),
 867                "H/C": mformula.H_C,
 868                "O/C": mformula.O_C,
 869                "Ion Type": eval("mformula.ion_type.lower(){}".format(encode)),
 870                "Is Isotopologue": int(mformula.is_isotopologue),
 871                "Molecular Formula": eval("mformula.string{}".format(encode)),
 872            }
 873            if additional_columns is not None:
 874                possible_dict = {
 875                    "Aromaticity Index": mformula.A_I,
 876                    "NOSC": mformula.nosc,
 877                    "Aromaticity Index (modified)": mformula.A_I_mod,
 878                }
 879                for column in additional_columns:
 880                    dict_result[column] = possible_dict.get(column)
 881
 882            if mformula.adduct_atom:
 883                dict_result["Adduct"] = eval("mformula.adduct_atom{}".format(encode))
 884
 885            if mformula.is_isotopologue:
 886                dict_result["Mono Isotopic Index"] = mformula.mspeak_index_mono_isotopic
 887
 888            if self.atoms_order_list is None:
 889                atoms_order_list = self.get_all_used_atoms_in_order(mass_spectrum)
 890            else:
 891                atoms_order_list = self.atoms_order_list
 892
 893            for atom in atoms_order_list:
 894                if atom in formula_dict.keys():
 895                    dict_result[atom] = formula_dict.get(atom)
 896
 897            dict_data_list.append(dict_result)
 898
 899        score_methods = mass_spectrum.molecular_search_settings.score_methods
 900        selected_score_method = (
 901            mass_spectrum.molecular_search_settings.output_score_method
 902        )
 903
 904        if selected_score_method in score_methods:
 905            # temp set score method as the one chosen in the output
 906            current_method = mass_spectrum.molecular_search_settings.score_method
 907            mass_spectrum.molecular_search_settings.score_method = selected_score_method
 908
 909            for index, ms_peak in enumerate(mass_spectrum):
 910                # print(ms_peak.mz_exp)
 911
 912                if ms_peak:
 913                    m_formula = ms_peak.best_molecular_formula_candidate
 914
 915                    if m_formula:
 916                        if not m_formula.is_isotopologue:
 917                            add_match_dict_data(
 918                                index,
 919                                ms_peak,
 920                                m_formula,
 921                                additional_columns=additional_columns,
 922                            )
 923
 924                            for (
 925                                iso_mspeak_index,
 926                                iso_mf_formula,
 927                            ) in m_formula.mspeak_mf_isotopologues_indexes:
 928                                iso_ms_peak = mass_spectrum[iso_mspeak_index]
 929                                add_match_dict_data(
 930                                    iso_mspeak_index,
 931                                    iso_ms_peak,
 932                                    iso_mf_formula,
 933                                    additional_columns=additional_columns,
 934                                )
 935                else:
 936                    if include_no_match and no_match_inline:
 937                        add_no_match_dict_data(index, ms_peak)
 938
 939            if include_no_match and not no_match_inline:
 940                for index, ms_peak in enumerate(mass_spectrum):
 941                    if not ms_peak:
 942                        add_no_match_dict_data(index, ms_peak)
 943            # reset score method as the one chosen in the output
 944            mass_spectrum.molecular_search_settings.score_method = current_method
 945
 946        else:
 947            for index, ms_peak in enumerate(mass_spectrum):
 948                # check if there is a molecular formula candidate for the msPeak
 949
 950                if ms_peak:
 951                    # m_formula = ms_peak.molecular_formula_lowest_error
 952                    for m_formula in ms_peak:
 953                        if mass_spectrum.molecular_search_settings.output_min_score > 0:
 954                            if (
 955                                m_formula.confidence_score
 956                                >= mass_spectrum.molecular_search_settings.output_min_score
 957                            ):
 958                                if m_formula.is_isotopologue:  # isotopologues inline
 959                                    if include_isotopologues and isotopologue_inline:
 960                                        add_match_dict_data(
 961                                            index,
 962                                            ms_peak,
 963                                            m_formula,
 964                                            additional_columns=additional_columns,
 965                                        )
 966                                else:
 967                                    add_match_dict_data(
 968                                        index,
 969                                        ms_peak,
 970                                        m_formula,
 971                                        additional_columns=additional_columns,
 972                                    )  # add monoisotopic peak
 973
 974                            # cutoff because of low score
 975                            else:
 976                                add_no_match_dict_data(index, ms_peak)
 977
 978                        else:
 979                            if m_formula.is_isotopologue:  # isotopologues inline
 980                                if include_isotopologues and isotopologue_inline:
 981                                    add_match_dict_data(
 982                                        index,
 983                                        ms_peak,
 984                                        m_formula,
 985                                        additional_columns=additional_columns,
 986                                    )
 987                            else:
 988                                add_match_dict_data(
 989                                    index,
 990                                    ms_peak,
 991                                    m_formula,
 992                                    additional_columns=additional_columns,
 993                                )  # add monoisotopic peak
 994                else:
 995                    # include not_match
 996                    if include_no_match and no_match_inline:
 997                        add_no_match_dict_data(index, ms_peak)
 998
 999            if include_isotopologues and not isotopologue_inline:
1000                for index, ms_peak in enumerate(mass_spectrum):
1001                    for m_formula in ms_peak:
1002                        if m_formula.is_isotopologue:
1003                            if (
1004                                m_formula.confidence_score
1005                                >= mass_spectrum.molecular_search_settings.output_min_score
1006                            ):
1007                                add_match_dict_data(
1008                                    index,
1009                                    ms_peak,
1010                                    m_formula,
1011                                    additional_columns=additional_columns,
1012                                )
1013
1014            if include_no_match and not no_match_inline:
1015                for index, ms_peak in enumerate(mass_spectrum):
1016                    if not ms_peak:
1017                        add_no_match_dict_data(index, ms_peak)
1018
1019        # remove duplicated add_match data possibly introduced on the output_score_filter step
1020        res = []
1021        [res.append(x) for x in dict_data_list if x not in res]
1022
1023        return res

Returns the mass spectrum data as a list of dictionaries.

Parameters
  • mass_spectrum (MassSpectrum): The mass spectrum to export.
  • include_no_match (bool, optional): Whether to include unassigned (no match) data. Defaults to True.
  • include_isotopologues (bool, optional): Whether to include isotopologues. Defaults to True.
  • isotopologue_inline (bool, optional): Whether to include isotopologues inline. Defaults to True.
  • no_match_inline (bool, optional): Whether to include unassigned (no match) data inline. Defaults to False.
  • is_hdf5 (bool, optional): Whether the mass spectrum is being exported to an HDF5 file. Defaults to False.
Returns
  • list: The mass spectrum data as a list of dictionaries.